Modèles C ++ qui n'acceptent que certains types

159

En Java, vous pouvez définir une classe générique qui n'accepte que 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 ++?

mgamère
la source
question déjà assez ancienne ... Je pense que ce qui manque ici (également dans les réponses) est que les génériques Java ne sont pas vraiment un équivalent des modèles en C ++. Il y a des similitudes, mais à mon avis, il faut faire attention à traduire directement une solution java en C ++ juste pour se rendre compte qu'elles sont peut-être faites pour différents types de problèmes;)
idclev 463035818

Réponses:

104

Je suggère d'utiliser la fonction d'assertion statique de Boost de concert avec is_base_ofla bibliothèque Boost Type Traits:

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 d'autres cas plus simples, vous pouvez simplement déclarer en avant un modèle global, mais ne le définir (explicitement ou partiellement) que 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.

[Modification mineure 12/06/2013: L'utilisation d'un modèle déclaré mais non défini entraînera des messages d'erreur dans l' éditeur de liens , et non dans le compilateur.]

j_random_hacker
la source
Les affirmations statiques sont également bien. :)
macbirdie
5
@John: J'ai peur que la spécialisation ne corresponde myBaseTypeexactement. Avant de rejeter Boost, vous devez savoir que la plupart d'entre eux sont du code de modèle d'en-tête uniquement - il n'y a donc pas de coût de mémoire ou de temps au moment de l'exécution pour les choses que vous n'utilisez pas. De plus, les choses particulières que vous utiliseriez ici ( BOOST_STATIC_ASSERT()et is_base_of<>) peuvent être implémentées en utilisant uniquement des déclarations (c'est-à-dire aucune définition réelle des fonctions ou des variables) afin qu'elles ne prennent pas non plus d'espace ou de temps.
j_random_hacker
50
C ++ 11 est arrivé. Maintenant, nous pouvons utiliser static_assert(std::is_base_of<List, T>::value, "T must extend list").
Siyuan Ren
2
BTW, la raison pour laquelle la double parenthèse est nécessaire est que BOOST_STATIC_ASSERT est une macro et que les parenthèses supplémentaires empêchent le préprocesseur d'interpréter la virgule dans les arguments de la fonction is_base_of comme un deuxième argument de macro.
jfritz42
1
@Andreyua: Je ne comprends pas vraiment ce qui manque. Vous pouvez essayer de déclarer une variable my_template<int> x;ou my_template<float**> y;et vérifier que le compilateur les autorise, puis déclarer une variable my_template<char> z;et vérifier que ce n'est pas le cas.
j_random_hacker
134

Cela n'est généralement pas justifié en C ++, comme l'ont noté d'autres réponses ici. En C ++, nous avons tendance à définir des types génériques basés sur d'autres contraintes autres que «hérite de cette classe». Si vous vouliez 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 casse beaucoup de concepts auxquels les gens s'attendent en C ++. Il est préférable d'utiliser des astuces comme définir vos propres traits. Par exemple, observable_listsouhaite peut-être accepter tout type de conteneur qui a les typedefs const_iteratoret une fonction membre beginet endqui retourne const_iterator. Si vous limitez cela aux classes qui héritent de, listun utilisateur qui a son propre type qui n'hérite pas de listmais fournit ces fonctions membres et typedefs ne pourra pas utiliser votreobservable_list .

Il existe deux solutions à ce problème, l'une d'elles est de ne rien contraindre et de s'appuyer sur le typage canard. Un gros inconvénient de cette solution est qu'elle implique une quantité massive d'erreurs qui peuvent être difficiles à gérer pour les utilisateurs. Une autre solution consiste à définir des traits pour contraindre le type fourni pour répondre aux exigences de l'interface. Le gros inconvénient de cette solution est qu'elle implique une écriture supplémentaire qui peut être considérée comme ennuyeuse. Cependant, le côté positif est que vous serez en mesure d'écrire vos propres messages d'erreur a la static_assert.

Par souci d'exhaustivité, 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...
};

Il existe de nombreux concepts illustrés dans l'exemple ci-dessus 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 traits de type.

Rapptz
la source
2
Je n'ai jamais réalisé que les modèles C ++ utilisent le typage canard jusqu'à aujourd'hui. Un peu bizarre!
Andy
2
Étant donné les contraintes de politique étendues que C ++ a introduites dans C , je ne sais pas pourquoi template<class T:list>un concept aussi offensant. Merci pour le conseil.
bvj
61

La solution simple, que personne n'a encore mentionnée, consiste simplement à ignorer le problème. Si j'essaie d'utiliser un intcomme type de modèle dans un modèle de fonction qui attend une classe de conteneur telle que vecteur ou liste, j'obtiendrai une erreur de compilation. Cru et simple, mais cela résout le problème. Le compilateur essaiera d'utiliser le type que vous spécifiez, et si cela échoue, il génère une erreur de compilation.

Le seul problème avec cela est que les messages d'erreur que vous obtenez seront difficiles à lire. C'est néanmoins une manière très courante de le faire. La bibliothèque standard est pleine de modèles de fonctions ou de classes qui attendent un certain comportement du type de modèle et ne font rien pour vérifier que les types utilisés sont valides.

Si vous voulez des messages d'erreur plus agréables (ou si vous voulez attraper des cas qui ne produiraient pas d'erreur de compilation, mais qui n'ont toujours pas de sens), vous pouvez, en fonction de la complexité que vous souhaitez faire, 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.

jalf
la source
7
Oui, j'ai toujours pensé que les modèles sont ce qui se rapproche le plus de la saisie en C ++. S'il possède tous les éléments nécessaires à un modèle, il peut être utilisé dans un modèle.
@John: Je suis désolé, je ne peux pas faire la tête ou la queue. De quel type s'agit T-il et d'où s'appelle ce code? Sans un certain contexte, je n'ai aucune chance de comprendre cet extrait de code. Mais ce que j'ai dit est vrai. Si vous essayez d'appeler toString()un type qui n'a pas de toStringfonction membre, vous obtiendrez une erreur de compilation.
jalf
@John: la prochaine fois, vous devriez peut-être être un peu moins heureux de voter contre les gens lorsque le problème est dans votre code
jalf
@jalf, d'accord. +1. C'était une excellente réponse en essayant simplement d'en faire le meilleur. Désolé pour une mauvaise lecture. Je pensais que nous parlions d'utiliser le type comme paramètre pour les classes et non pour les modèles de fonction, qui, je suppose, sont membres du premier mais doivent être invoqués pour que le compilateur puisse marquer.
John
13

Nous pouvons utiliser std::is_base_ofet std::enable_if:
( static_assertpeut être supprimé, les classes ci-dessus peuvent être implémentées sur mesure ou utilisées à partir de boost si nous ne pouvons pas référencer 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
}
firda
la source
13

Autant que je sache, ce n'est actuellement pas possible en C ++. Cependant, il est prévu d'ajouter une fonctionnalité appelée «concepts» dans la nouvelle norme C ++ 0x qui fournit la fonctionnalité que vous recherchez. Cet article Wikipédia sur les concepts C ++ l'expliquera plus en détail.

Je sais que cela ne résout pas votre problème immédiat, mais il existe des compilateurs C ++ qui ont déjà commencé à ajouter des fonctionnalités à partir de la nouvelle norme, il est donc possible de trouver un compilateur qui a déjà implémenté la fonctionnalité de concepts.

Barry Carr
la source
4
Les concepts ont malheureusement été supprimés de la norme.
macbirdie
4
Les contraintes et les concepts doivent être adoptés pour C ++ 20.
Petr Javorik
C'est possible même sans concepts, en utilisant static_assertet SFINAE, comme le montrent les autres réponses. Le problème restant pour quelqu'un venant de Java ou C #, ou Haskell (...) est que le compilateur C ++ 20 ne vérifie pas la définition par rapport aux concepts requis, ce que font Java et C #.
user7610
10

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 les mêmes que les modèles ; ils utilisent l' effacement de type , qui est une technique dynamique , plutôt que le polymorphisme au moment de la compilation , qui est une technique statique . Il devrait être évident pourquoi ces deux tactiques très différentes ne gèlent pas bien.

Plutôt que d'essayer d'utiliser une construction au moment de la compilation pour simuler une construction au moment de l'exécution, regardons ce qui extendsfait réellement: selon Stack Overflow et Wikipedia , extend est utilisé pour indiquer le sous-classement.

C ++ prend également en charge le sous-classement.

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

Emballons-le dans un typedef, pour le rendre plus facile à utiliser, plutôt que de créer une classe entière, et voilà:

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émente rien d'autre 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 également aussi lentement que les génériques de style Java en raison des recherches de table virtuelle, mais cela peut souvent être une perte acceptable.

Alice
la source
3
Je ne suis pas fan des réponses qui utilisent des expressions telles que «cela devrait être évident», ou «tout le monde sait», puis expliquent ce qui est évident ou universellement connu. L'évident est relatif au contexte, à l'expérience et au contexte de l'expérience. De telles déclarations sont par nature grossières.
3Dave
2
@DavidLively Il est environ deux ans trop tard pour critiquer cette réponse pour l'étiquette, mais je suis également en désaccord avec vous dans ce cas précis; J'ai expliqué pourquoi ces deux techniques ne vont pas ensemble avant de déclarer que c'était évident, pas après. J'ai fourni le contexte, puis j'ai dit que la conclusion tirée de ce contexte était évidente. Cela ne correspond pas exactement à votre moule.
Alice
L'auteur de cette réponse a déclaré que quelque chose était évident après avoir fait du gros travail. Je ne pense pas que l'auteur ait eu l'intention de dire que la solution était évidente.
Luke Gehorsam
10

Un équivalent qui n'accepte que 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
{
    // ...
};
nh_
la source
8

Résumé: ne faites pas ça.

La réponse de j_random_hacker vous indique comment faire cela. Cependant, je voudrais également souligner que vous ne devriez pas faire cela. L'intérêt des modèles est qu'ils peuvent accepter n'importe quel type compatible, et les contraintes de type de style Java rompent cela.

Les contraintes de type de Java sont un bogue pas 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 une telle restriction. Les types de paramètres de modèle peuvent être de tout 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 fait 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 deux 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 prennent en charge + =.

Aucune boxe / déballage des primitives n'est nécessaire.

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

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

catphive
la source
2
Si vous suggérez de ne jamais spécialiser les modèles, pouvez-vous également expliquer pourquoi il est dans la langue?
1
Je comprends votre point de vue, mais si votre argument de modèle doit être dérivé d'un type spécifique, il vaut mieux avoir un message facile à interpréter de static_assert que l'erreur normale du compilateur vomit.
jhoffman0x
1
Oui, C ++ est plus expressif ici, mais si c'est généralement une bonne chose (car on peut exprimer plus avec moins), parfois on veut limiter délibérément le pouvoir qu'on se donne, pour avoir la certitude que l'on comprend parfaitement un système.
j_random_hacker
La spécialisation de type @Curg est utile lorsque vous voulez pouvoir profiter de quelque chose qui ne peut être fait que pour certains types. par exemple, un booléen est ~ normalement ~ un octet chacun, même si un octet peut ~ normalement ~ contenir 8 bits / booléens; une classe de collection de modèles peut (et dans le cas de std :: map le fait) se spécialiser pour boolean afin de pouvoir regrouper les données plus étroitement pour conserver la mémoire.
thecoshman
De plus, pour clarifier, cette réponse ne dit pas "ne jamais spécialiser les modèles", mais n'utilise pas cette fonctionnalité pour essayer de limiter les types pouvant être utilisés avec un modèle.
thecoshman
6

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

Depuis C ++ 20, les concepts deviennent une caractéristique officielle du langage.

macbirdie
la source
2
Eh bien, il est possible, mais la vérification du concept est toujours une bonne idée. :)
j_random_hacker
Je voulais dire en fait que ce n'était pas possible en C ++ "simple". ;)
macbirdie
5
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.

Stuart
la source
@Zehelvion Type::FooSecurityest utilisé dans la classe de modèle. Si la classe, passée en argument modèle, ne l'a pas fait FooSecurity, tenter de l'utiliser provoque une erreur. Il est certain que si la classe passée dans l'argument template n'a pas FooSecurity, elle n'est pas dérivée Base.
GingerPlusPlus
2

Utilisation du concept C ++ 20

https://en.cppreference.com/w/cpp/language/constraints cppreference donne le cas d'utilisation de l'héritage comme exemple de concept explicite:

template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Base> T>
void f(T);  // T is constrained by Derived<T, Base>

Pour plusieurs bases, je suppose que la syntaxe sera:

template <class T, class U, class V>
concept Derived = std::is_base_of<U, T>::value || std::is_base_of<V, T>::value;
 
template<Derived<Base1, Base2> T>
void f(T);

GCC 10 semble l'avoir implémenté: https://gcc.gnu.org/gcc-10/changes.html et vous pouvez l'obtenir en tant que PPA sur Ubuntu 20.04 . https://godbolt.org/ Mon GCC 10.1 local ne l'a pas conceptencore reconnu , donc je ne sais pas ce qui se passe.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
1

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 (voire meilleurs).

J'ai parcouru du code STL (sous Linux, je pense que c'est celui dérivant de l'implémentation de SGI). Il contient des «affirmations de concept»; par exemple, si vous avez besoin d'un type qui comprend *xet ++x, l'assertion de concept contiendra ce code dans une fonction ne rien faire (ou quelque chose de similaire). Cela nécessite une surcharge, il peut donc être judicieux de le mettre dans une macro dont la définition dépend de#ifdef debug .

Si la relation de sous-classe est vraiment ce que vous voulez savoir, vous pouvez l'affirmer dans le constructeur T instanceof list(sauf qu'elle est «orthographiée» différemment en C ++). De cette façon, vous pouvez tester votre façon de sortir du compilateur sans pouvoir le vérifier pour vous.

Jonas Kölker
la source
1

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

(1) Si vous voulez qu'un modèle de fonction n'accepte que 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 vice versa, ou si vous souhaitez filtrer les classes d'une autre manière, appelez une fonction d'assistance de modèle (vide) dans votre fonction qui n'est définie que pour les classes que vous souhaitez accepter.

Vous pouvez également utiliser (1) et (2) dans les fonctions membres d'une classe pour forcer ces vérifications de type sur toute la classe.

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

Jaap
la source
-2

Eh bien, vous pouvez créer votre modèle en lisant quelque chose comme ceci:

template<typename T>
class ObservableList {
  std::list<T> contained_data;
};

Cela rendra cependant la restriction implicite, et vous ne pouvez pas simplement fournir quelque chose qui ressemble à une liste. Il existe d'autres moyens de restreindre les types de conteneurs utilisés, par exemple en utilisant des types d'itérateurs spécifiques qui n'existent pas dans tous les conteneurs, mais encore une fois, c'est plus une restriction implicite qu'une restriction explicite.

À ma connaissance, une construction qui refléterait l'instruction Java dans toute son étendue n'existe pas dans la norme actuelle.

Il existe des moyens de restreindre les types que vous pouvez utiliser dans un modèle que vous écrivez en utilisant des typedefs spécifiques à l'intérieur de votre modèle. Cela garantira que la compilation de la spécialisation de modèle pour un type qui n'inclut pas ce typedef particulier échouera, de sorte que vous pouvez sélectivement prendre en charge / ne pas prendre en charge certains types.

En C ++ 11, l'introduction de concepts devrait rendre cela plus facile, mais je ne pense pas que cela fera exactement ce que vous voudriez non plus.

Timo Geusch
la source