web-dev-qa-db-fra.com

Modèles C ++ n'acceptant que certains types

Dans Java vous pouvez définir une classe générique qui accepte uniquement les types qui étendent la classe de votre choix, par exemple:

public class ObservableList<T extends List> {
  ...
}

Ceci est fait en utilisant le mot clé "extend".

Existe-t-il un équivalent simple à ce mot clé en C++?

137
mgamer

Je suggère d'utiliser la fonction statique assert de Boost de concert avec is_base_of de la bibliothèque Traits de type Boost:

template<typename T>
class ObservableList {
    BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
    ...
};

Dans certains cas plus simples, vous pouvez simplement déclarer un modèle global en aval, mais uniquement le définir (en le explicitant ou en le spécialisant partiellement) pour les types valides:

template<typename T> class my_template;     // Declare, but don't define

// int is a valid type
template<> class my_template<int> {
    ...
};

// All pointer types are valid
template<typename T> class my_template<T*> {
    ...
};

// All other types are invalid, and will cause linker error messages.

[Minor EDIT 6/12/2013: L'utilisation d'un modèle déclaré mais non défini entraînera éditeur de liens, pas compilateur, messages d'erreur.]

100
j_random_hacker

Cela est généralement injustifié en C++, comme d'autres réponses l'ont noté. En C++, nous avons tendance à définir des types génériques basés sur d'autres contraintes que "hérite de cette classe". Si vous voulez vraiment faire cela, c'est assez facile à faire en C++ 11 et <type_traits>:

#include <type_traits>

template<typename T>
class observable_list {
    static_assert(std::is_base_of<list, T>::value, "T must inherit from list");
    // code here..
};

Cela rompt toutefois beaucoup les concepts attendus en C++. Il est préférable d'utiliser des astuces comme définir vos propres traits. Par exemple, peut-être que observable_list Souhaite accepter tout type de conteneur contenant les typedefs const_iterator Et une fonction membre begin et end renvoyant const_iterator. Si vous limitez cela aux classes qui héritent de list, un utilisateur ayant son propre type n'héritant pas de list mais fournissant ces fonctions membres et typedefs ne pourrait pas utiliser votre observable_list.

Il y a deux solutions à ce problème, l'une d'entre elles est de ne rien contraindre et de s'appuyer sur la frappe au canard. Un gros inconvénient de cette solution est qu’elle implique une quantité énorme d’erreurs qui peuvent être difficiles à comprendre pour les utilisateurs. Une autre solution consiste à définir des traits pour contraindre le type fourni à répondre aux exigences de l'interface. Le gros inconvénient de cette solution est que cela implique une écriture supplémentaire qui peut être considérée comme agaçante. Toutefois, le point positif est que vous pourrez écrire vos propres messages d'erreur à la static_assert.

Pour être complet, la solution à l'exemple ci-dessus est donnée:

#include <type_traits>

template<typename...>
struct void_ {
    using type = void;
};

template<typename... Args>
using Void = typename void_<Args...>::type;

template<typename T, typename = void>
struct has_const_iterator : std::false_type {};

template<typename T>
struct has_const_iterator<T, Void<typename T::const_iterator>> : std::true_type {};

struct has_begin_end_impl {
    template<typename T, typename Begin = decltype(std::declval<const T&>().begin()),
                         typename End   = decltype(std::declval<const T&>().end())>
    static std::true_type test(int);
    template<typename...>
    static std::false_type test(...);
};

template<typename T>
struct has_begin_end : decltype(has_begin_end_impl::test<T>(0)) {};

template<typename T>
class observable_list {
    static_assert(has_const_iterator<T>::value, "Must have a const_iterator typedef");
    static_assert(has_begin_end<T>::value, "Must have begin and end member functions");
    // code here...
};

L'exemple ci-dessus présente de nombreux concepts qui présentent les fonctionnalités de C++ 11. Certains termes de recherche pour les curieux sont les modèles variadiques, SFINAE, l’expression SFINAE et les caractères typographiques.

103
Rapptz

La solution simple, que personne n’a encore mentionnée, est d’ignorer le problème. Si j'essaie d'utiliser int comme type de modèle dans un modèle de fonction qui attend une classe de conteneur telle que vector ou list, j'obtiendrai une erreur de compilation. Brut et simple, mais cela résout le problème. Le compilateur essaiera d'utiliser le type que vous spécifiez et, en cas d'échec, une erreur de compilation est générée.

Le seul problème avec cela est que les messages d'erreur que vous recevez vont être difficiles à lire. C'est néanmoins une façon très courante de faire cela. La bibliothèque standard est pleine de modèles de fonction ou de classe qui attendent un comportement du type de modèle et ne vérifient pas que les types utilisés sont valides.

Si vous voulez des messages d'erreur plus intéressants (ou si vous voulez intercepter des cas qui ne produiraient pas d'erreur de compilateur, mais qui n'ont pas de sens), vous pouvez, en fonction de la complexité de votre tâche, utiliser l'assertion statique de Boost ou la bibliothèque Boost concept_check.

Avec un compilateur à jour, vous avez un Built_in static_assert, qui pourrait être utilisé à la place.

56
jalf

Pour autant que je sache, cela n’est actuellement pas possible en C++. Toutefois, il est prévu d’ajouter une fonctionnalité appelée "concepts" dans la nouvelle norme C++ 0x qui fournira la fonctionnalité que vous recherchez. Ceci article Wikipedia à propos de C++ Concepts l'expliquera plus en détail.

Je sais que cela ne résout pas votre problème immédiat, mais certains compilateurs C++ ont déjà commencé à ajouter des fonctionnalités du nouveau standard. Il est donc possible de trouver un compilateur ayant déjà implémenté la fonctionnalité de concepts.

13
Barry Carr

On peut utiliser std::is_base_of et std::enable_if :
( static_assert peut être supprimé, les classes ci-dessus peuvent être implémentées et utilisées à partir de boost si nous ne pouvons pas faire référence à type_traits )

#include <type_traits>
#include <list>

class Base {};
class Derived: public Base {};

#if 0   // wrapper
template <class T> class MyClass /* where T:Base */ {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
    typename std::enable_if<std::is_base_of<Base, T>::value, T>::type inner;
};
#Elif 0 // base class
template <class T> class MyClass: /* where T:Base */
    protected std::enable_if<std::is_base_of<Base, T>::value, T>::type {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
};
#Elif 1 // list-of
template <class T> class MyClass /* where T:list<Base> */ {
    static_assert(std::is_base_of<Base, typename T::value_type>::value , "T::value_type is not derived from Base");
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type base; 
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type::value_type value_type;

};
#endif

int main() {
#if 0   // wrapper or base-class
    MyClass<Derived> derived;
    MyClass<Base> base;
//  error:
    MyClass<int> wrong;
#Elif 1 // list-of
    MyClass<std::list<Derived>> derived;
    MyClass<std::list<Base>> base;
//  error:
    MyClass<std::list<int>> wrong;
#endif
//  all of the static_asserts if not commented out
//  or "error: no type named ‘type’ in ‘struct std::enable_if<false, ...>’ pointing to:
//  1. inner
//  2. MyClass
//  3. base + value_type
}
11
firda

Un équivalent qui accepte uniquement les types T dérivés du type List ressemble à

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};
10
nh_

Je pense que toutes les réponses précédentes ont perdu de vue la forêt pour les arbres.

Les génériques Java ne sont pas identiques aux modèles; ils utilisent type effacement, qui est une technique dynamique , plutôt que polymorphisme du temps de compilation, qui est technique statique . Il devrait être évident pourquoi ces deux tactiques très différentes ne gélifient pas bien.

Plutôt que d’essayer d’utiliser une construction de compilation pour simuler une exécution, regardons ce que extends fait réellement: selon le dépassement de pile et Wikipedia , expand est utilisé pour indiquer le sous-classement.

C++ prend également en charge les sous-classes.

Vous affichez également une classe de conteneur, qui utilise l'effacement de type sous la forme d'un générique et qui s'étend pour effectuer une vérification de type. En C++, vous devez faire le type machine d'effacement vous-même, ce qui est simple: faites un pointeur sur la super-classe.

Enveloppons-le dans un typedef, pour le rendre plus facile à utiliser, plutôt que de faire une classe entière, et le tour est joué:

typedef std::list<superclass*> subclasses_of_superclass_only_list;

Par exemple:

class Shape { };
class Triangle : public Shape { };

typedef std::list<Shape*> only_shapes_list;
only_shapes_list shapes;

shapes.Push_back(new Triangle()); // Works, triangle is kind of shape
shapes.Push_back(new int(30)); // Error, int's are not shapes

Maintenant, il semble que List soit une interface, représentant une sorte de collection. Une interface en C++ serait simplement une classe abstraite, c'est-à-dire une classe qui n'implémenterait que des méthodes virtuelles pures. En utilisant cette méthode, vous pouvez facilement implémenter votre exemple Java en C++, sans aucun concept ni spécialisation de modèle. Il fonctionnerait aussi lentement que Java) à la table virtuelle, mais cela peut souvent être une perte acceptable.

7
Alice

Résumé: Ne faites pas ça.

la réponse de j_random_hacker vous dit comment faire ceci. Cependant, je voudrais aussi souligner que vous devriez pas faire ceci. L’intérêt des modèles est qu’ils peuvent accepter n’importe quel type compatible et que les contraintes de type Java du type Java $ le rompent).

Les contraintes de type de Java sont un bug et non une fonctionnalité. Ils sont là parce que Java) efface le type sur les génériques, donc Java ne peut pas comprendre comment appeler des méthodes en se basant uniquement sur la valeur des paramètres de type.

C++ d'autre part n'a pas de telle restriction. Les types de paramètre de modèle peuvent être n'importe quel type compatible avec les opérations avec lesquelles ils sont utilisés. Il n'est pas nécessaire qu'il y ait une classe de base commune. Ceci est similaire au "Duck Typing" de Python, mais effectué au moment de la compilation.

Un exemple simple montrant la puissance des modèles:

// Sum a vector of some type.
// Example:
// int total = sum({1,2,3,4,5});
template <typename T>
T sum(const vector<T>& vec) {
    T total = T();
    for (const T& x : vec) {
        total += x;
    }
    return total;
}

Cette fonction de somme peut additionner un vecteur de tout type prenant en charge les opérations correctes. Il fonctionne avec les primitives telles que int/long/float/double et les types numériques définis par l'utilisateur qui surchargent l'opérateur + =. Heck, vous pouvez même utiliser cette fonction pour joindre des chaînes, car elles supportent + =.

Aucune boxe/unboxing de primitives n'est nécessaire.

Notez qu'il construit également de nouvelles instances de T en utilisant T (). Ceci est trivial en C++ avec des interfaces implicites, mais pas vraiment possible dans Java avec des contraintes de type.

Bien que les modèles C++ n'aient pas de contraintes de type explicites, ils sont néanmoins sécurisés au type et ne seront pas compilés avec un code qui ne prend pas en charge les opérations correctes.

6
catphive

Ce n'est pas possible en langage C++ brut, mais vous pouvez vérifier les paramètres du modèle au moment de la compilation via Concept Checking, par exemple. en utilisant BCCL de Boost .

5
macbirdie
class Base
{
    struct FooSecurity{};
};

template<class Type>
class Foo
{
    typename Type::FooSecurity If_You_Are_Reading_This_You_Tried_To_Create_An_Instance_Of_Foo_For_An_Invalid_Type;
};

Assurez-vous que les classes dérivées héritent de la structure FooSecurity et que le compilateur sera bouleversé aux bons endroits.

5
Stuart

Existe-t-il un équivalent simple à ce mot clé en C++?

Non.

Selon ce que vous essayez d'accomplir, il pourrait y avoir des substituts adéquats (ou même meilleurs).

J'ai parcouru du code STL (sur linux, je pense que c'est celui qui découle de l'implémentation de SGI). Il a des "affirmations de concept"; par exemple, si vous avez besoin d'un type qui comprend *x et ++x, l'assertion de concept contiendrait ce code dans une fonction ne rien faire (ou quelque chose de similaire). Cela nécessite des frais généraux, il peut donc être judicieux de le placer dans une macro dont la définition dépend de #ifdef debug.

Si vous souhaitez vraiment connaître la relation entre les sous-classes, vous pouvez affirmer dans le constructeur que T instanceof list (sauf que "orthographié" différemment en C++). De cette façon, vous pouvez vous sortir du compilateur sans pouvoir le vérifier pour vous.

1
Jonas Kölker

Il n'y a pas de mot-clé pour de telles vérifications de type, mais vous pouvez y insérer du code qui échouera au moins de manière ordonnée:

(1) Si vous souhaitez qu'un modèle de fonction accepte uniquement les paramètres d'une certaine classe de base X, affectez-le à une référence X dans votre fonction. (2) Si vous souhaitez accepter des fonctions mais pas des primitives ou inversement, ou si vous souhaitez filtrer les classes d'une autre manière, appelez une fonction d'assistance de modèle (vide) au sein de votre fonction, définie uniquement pour les classes que vous souhaitez accepter.

Vous pouvez également utiliser (1) et (2) dans les fonctions membres d'une classe pour imposer ces vérifications de type à la classe entière.

Vous pouvez probablement le mettre dans une macro intelligente pour soulager votre douleur. :)

1
Jaap