@sbi: S'il fait ça, il trouvera sa propre question. Et ce serait curieusement récurrent. :)
Craig McQueen
1
BTW, il me semble que le terme devrait être "curieusement récurrent". Suis-je mal compris le sens?
Craig McQueen
1
Craig: Je pense que vous l'êtes; c'est "curieusement récurrent" dans le sens où il a été constaté qu'il surgissait dans de multiples contextes.
Gareth McCaughan
Réponses:
276
En bref, CRTP se produit lorsqu'une classe Aa une classe de base qui est une spécialisation de modèle pour la classe Aelle - même. Par exemple
template<class T>class X{...};class A :public X<A>{...};
Il est récurrent curieusement, est - ce pas? :)
Maintenant, qu'est-ce que cela vous donne? Cela donne en fait au Xmodèle la possibilité d'être une classe de base pour ses spécialisations.
Par exemple, vous pouvez créer une classe singleton générique (version simplifiée) comme celle-ci
template<classActualClass>classSingleton{public:staticActualClass&GetInstance(){if(p ==nullptr)
p =newActualClass;return*p;}protected:staticActualClass* p;private:Singleton(){}Singleton(Singletonconst&);Singleton&operator=(Singletonconst&);};template<class T>
T*Singleton<T>::p =nullptr;
Maintenant, pour faire d'une classe arbitraire Aun singleton, vous devez faire ceci
class A:publicSingleton<A>{//Rest of functionality for class A};
Donc tu vois? Le modèle singleton suppose que sa spécialisation pour n'importe quel type Xsera héritée de singleton<X>et aura donc tous ses membres (publics, protégés) accessibles, y compris le GetInstance! Il existe d'autres utilisations utiles du CRTP. Par exemple, si vous voulez compter toutes les instances qui existent actuellement pour votre classe, mais que vous voulez encapsuler cette logique dans un modèle séparé (l'idée d'une classe concrète est assez simple - avoir une variable statique, incrémenter en ctors, décrémenter en dtors ). Essayez de le faire comme un exercice!
Encore un autre exemple utile, pour Boost (je ne sais pas comment ils l'ont implémenté, mais CRTP le fera aussi). Imaginez que vous souhaitiez fournir uniquement un opérateur <pour vos classes mais automatiquement un opérateur ==pour elles!
vous pouvez le faire comme ceci:
template<classDerived>classEquality{};template<classDerived>booloperator==(Equality<Derived>const& op1,Equality<Derived>const& op2){Derivedconst& d1 =static_cast<Derivedconst&>(op1);//you assume this works //because you know that the dynamic type will actually be your template parameter.//wonderful, isn't it?Derivedconst& d2 =static_cast<Derivedconst&>(op2);return!(d1 < d2)&&!(d2 < d1);//assuming derived has operator <}
Cela pourrait sembler que vous écririez moins si vous venez d' écrire l' opérateur ==pour Apple, mais imaginez que le Equalitymodèle fournirait non seulement ==mais >, >=, <=etc. Et vous pouvez utiliser ces définitions pour plusieurs cours, la réutilisation du code!
Cet article ne préconise pas le singleton comme un bon modèle de programmation, il l'utilise simplement comme une illustration qui peut être généralement comprise.imo the-1 est injustifié
John Dibling
3
@Armen: La réponse explique le CRTP d'une manière qui peut être comprise clairement, c'est une bonne réponse, merci pour une si belle réponse.
Alok Enregistrer
1
@Armen: merci pour cette excellente explication. J'avais en quelque sorte le CRTP avant, mais l'exemple de l'égalité a été éclairant! +1
Paul
1
Encore un autre exemple d'utilisation de CRTP est lorsque vous avez besoin d'une classe non copiable: template <class T> class NonCopyable {protected: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); NonCopyable & operator = (const NonCopyable &); }; Ensuite, vous utilisez noncopyable comme ci-dessous: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren
2
@Puppy: Singleton n'est pas terrible. Il est de loin surutilisé par les programmeurs en dessous de la moyenne alors que d'autres approches seraient plus appropriées, mais le fait que la plupart de ses utilisations soient terribles ne rend pas le modèle lui-même terrible. Il y a des cas où le singleton est la meilleure option, bien que ceux-ci soient rares.
Kaiserludi
47
Ici vous pouvez voir un excellent exemple. Si vous utilisez la méthode virtuelle, le programme saura ce qui s'exécute à l'exécution. Implémenter CRTP c'est le compilateur qui décide au moment de la compilation !!! C'est une belle performance!
template<class T>classWriter{public:Writer(){}~Writer(){}void write(constchar* str)const{static_cast<const T*>(this)->writeImpl(str);//here the magic is!!!}};classFileWriter:publicWriter<FileWriter>{public:FileWriter(FILE* aFile){ mFile = aFile;}~FileWriter(){ fclose(mFile);}//here comes the implementation of the write method on the subclassvoid writeImpl(constchar* str)const{
fprintf(mFile,"%s\n", str);}private:FILE* mFile;};classConsoleWriter:publicWriter<ConsoleWriter>{public:ConsoleWriter(){}~ConsoleWriter(){}void writeImpl(constchar* str)const{
printf("%s\n", str);}};
Ne pourriez-vous pas faire cela en définissant virtual void write(const char* str) const = 0;? Bien que pour être juste, cette technique semble très utile lorsque vous writeeffectuez un autre travail.
atlex2
26
En utilisant une méthode virtuelle pure, vous résolvez l'héritage au moment de l'exécution au lieu de la compilation. CRTP est utilisé pour résoudre ce problème au moment de la compilation afin que l'exécution soit plus rapide.
GutiMac
1
Essayez de créer une fonction simple qui attend un Writer abstrait: vous ne pouvez pas le faire car il n'y a aucune classe nommée Writer nulle part, alors où est votre polymorphisme exactement? Ce n'est pas du tout équivalent aux fonctions virtuelles et c'est beaucoup moins utile.
22
CRTP est une technique pour implémenter le polymorphisme à la compilation. Voici un exemple très simple. Dans l'exemple ci-dessous, ProcessFoo()travaille avec l' Baseinterface de classe et Base::Fooappelle la foo()méthode de l'objet dérivé , ce que vous souhaitez faire avec les méthodes virtuelles.
Cela pourrait également valoir la peine, dans cet exemple, d'ajouter un exemple de la façon d'implémenter un foo () par défaut dans la classe de base qui sera appelée si aucun Derived ne l'a implémenté. AKA changez foo dans la base en un autre nom (par exemple caller ()), ajoutez une nouvelle fonction foo () à la base qui coutait la "Base". Puis appelez caller () à l'intérieur de ProcessFoo
wizurd
@wizurd Cet exemple est davantage destiné à illustrer une fonction de classe de base virtuelle pure, c'est-à-dire que nous appliquons ce qui foo()est implémenté par la classe dérivée.
blueskin
3
C'est ma réponse préférée, car elle montre également pourquoi ce modèle est utile avec la ProcessFoo()fonction.
Pietro du
Je ne comprends pas l'intérêt de ce code, car avec void ProcessFoo(T* b)et sans avoir dérivé et dérivé d'un autre dérivé, il fonctionnerait toujours. À mon humble avis, il serait plus intéressant que ProcessFoo n'utilise pas de modèles d'une manière ou d'une autre.
Gabriel Devillers le
1
@GabrielDevillers Premièrement, le modèle ProcessFoo()fonctionnera avec n'importe quel type qui implémente l'interface, c'est-à-dire que dans ce cas, le type d'entrée T devrait avoir une méthode appelée foo(). Deuxièmement, pour qu'un non-modèle ProcessFoofonctionne avec plusieurs types, vous finirez probablement par utiliser RTTI, ce que nous voulons éviter. De plus, la version modélisée vous permet de vérifier le temps de compilation sur l'interface.
blueskin
6
Ce n'est pas une réponse directe, mais plutôt un exemple de la façon dont le CRTP peut être utile.
Un bon exemple concret de CRTP provient std::enable_shared_from_thisde C ++ 11:
Une classe Tpeut hériter de enable_shared_from_this<T>pour hériter des shared_from_thisfonctions membres qui obtiennent une shared_ptrinstance pointant vers *this.
Autrement dit, hériter de std::enable_shared_from_thispermet d'obtenir un pointeur partagé (ou faible) vers votre instance sans y avoir accès (par exemple à partir d'une fonction membre dont vous ne connaissez que l'existence *this).
C'est utile lorsque vous devez donner un std::shared_ptrmais que vous n'avez accès qu'à *this:
La raison pour laquelle vous ne pouvez pas simplement passer thisdirectement au lieu de shared_from_this()est que cela briserait le mécanisme de propriété:
struct S
{
std::shared_ptr<S> get_shared()const{return std::shared_ptr<S>(this);}};// Both shared_ptr think they're the only owner of S.// This invokes UB (double-free).
std::shared_ptr<S> s1 = std::make_shared<S>();
std::shared_ptr<S> s2 = s1->get_shared();
assert(s2.use_count()==1);
CRTP pourrait être utilisé pour implémenter le polymorphisme statique (qui aime le polymorphisme dynamique mais sans table de pointeur de fonction virtuelle).
désolé mon mauvais, static_cast s'occupe du changement. Si vous voulez quand même voir le cas d'angle même si cela ne cause pas d'erreur, voir ici: ideone.com/LPkktf
odinthenerd
30
Mauvais exemple. Ce code pourrait être fait sans vtables sans utiliser CRTP. Ce qui vtableest vraiment fourni, c'est d'utiliser la classe de base (pointeur ou référence) pour appeler des méthodes dérivées. Vous devriez montrer comment cela se fait avec CRTP ici.
Etherealone
17
Dans votre exemple, Base<>::method ()n'est même pas appelé, et vous n'utilisez le polymorphisme nulle part.
MikeMB
1
@Jichao, selon la note de @MikeMB, vous devriez appeler methodImplle nom methodde Baseet dans les classes dérivées methodImplau lieu demethod
Ivan Kush
1
si vous utilisez une méthode similaire (), elle est statiquement liée et vous n'avez pas besoin de la classe de base commune. Parce que de toute façon, vous ne pouvez pas l'utiliser de manière polymorphe via un pointeur de classe de base ou une référence. Le code devrait donc ressembler à ceci: #include <iostream> template <typename T> struct Writer {void write () {static_cast <T *> (this) -> writeImpl (); }}; struct Derived1: public Writer <Derived1> {void writeImpl () {std :: cout << "D1"; }}; struct Derived2: public Writer <Derived2> {void writeImpl () {std :: cout << "DER2"; }};
Réponses:
En bref, CRTP se produit lorsqu'une classe
A
a une classe de base qui est une spécialisation de modèle pour la classeA
elle - même. Par exempleIl est récurrent curieusement, est - ce pas? :)
Maintenant, qu'est-ce que cela vous donne? Cela donne en fait au
X
modèle la possibilité d'être une classe de base pour ses spécialisations.Par exemple, vous pouvez créer une classe singleton générique (version simplifiée) comme celle-ci
Maintenant, pour faire d'une classe arbitraire
A
un singleton, vous devez faire ceciDonc tu vois? Le modèle singleton suppose que sa spécialisation pour n'importe quel type
X
sera héritée desingleton<X>
et aura donc tous ses membres (publics, protégés) accessibles, y compris leGetInstance
! Il existe d'autres utilisations utiles du CRTP. Par exemple, si vous voulez compter toutes les instances qui existent actuellement pour votre classe, mais que vous voulez encapsuler cette logique dans un modèle séparé (l'idée d'une classe concrète est assez simple - avoir une variable statique, incrémenter en ctors, décrémenter en dtors ). Essayez de le faire comme un exercice!Encore un autre exemple utile, pour Boost (je ne sais pas comment ils l'ont implémenté, mais CRTP le fera aussi). Imaginez que vous souhaitiez fournir uniquement un opérateur
<
pour vos classes mais automatiquement un opérateur==
pour elles!vous pouvez le faire comme ceci:
Maintenant tu peux l'utiliser comme ça
Maintenant, vous n'avez pas fourni explicitement d'opérateur
==
pourApple
? Mais vous l'avez! Tu peux écrireCela pourrait sembler que vous écririez moins si vous venez d' écrire l' opérateur
==
pourApple
, mais imaginez que leEquality
modèle fournirait non seulement==
mais>
,>=
,<=
etc. Et vous pouvez utiliser ces définitions pour plusieurs cours, la réutilisation du code!CRTP est une chose merveilleuse :) HTH
la source
Ici vous pouvez voir un excellent exemple. Si vous utilisez la méthode virtuelle, le programme saura ce qui s'exécute à l'exécution. Implémenter CRTP c'est le compilateur qui décide au moment de la compilation !!! C'est une belle performance!
la source
virtual void write(const char* str) const = 0;
? Bien que pour être juste, cette technique semble très utile lorsque vouswrite
effectuez un autre travail.CRTP est une technique pour implémenter le polymorphisme à la compilation. Voici un exemple très simple. Dans l'exemple ci-dessous,
ProcessFoo()
travaille avec l'Base
interface de classe etBase::Foo
appelle lafoo()
méthode de l'objet dérivé , ce que vous souhaitez faire avec les méthodes virtuelles.http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e
Production:
la source
foo()
est implémenté par la classe dérivée.ProcessFoo()
fonction.void ProcessFoo(T* b)
et sans avoir dérivé et dérivé d'un autre dérivé, il fonctionnerait toujours. À mon humble avis, il serait plus intéressant que ProcessFoo n'utilise pas de modèles d'une manière ou d'une autre.ProcessFoo()
fonctionnera avec n'importe quel type qui implémente l'interface, c'est-à-dire que dans ce cas, le type d'entrée T devrait avoir une méthode appeléefoo()
. Deuxièmement, pour qu'un non-modèleProcessFoo
fonctionne avec plusieurs types, vous finirez probablement par utiliser RTTI, ce que nous voulons éviter. De plus, la version modélisée vous permet de vérifier le temps de compilation sur l'interface.Ce n'est pas une réponse directe, mais plutôt un exemple de la façon dont le CRTP peut être utile.
Un bon exemple concret de CRTP provient
std::enable_shared_from_this
de C ++ 11:Autrement dit, hériter de
std::enable_shared_from_this
permet d'obtenir un pointeur partagé (ou faible) vers votre instance sans y avoir accès (par exemple à partir d'une fonction membre dont vous ne connaissez que l'existence*this
).C'est utile lorsque vous devez donner un
std::shared_ptr
mais que vous n'avez accès qu'à*this
:La raison pour laquelle vous ne pouvez pas simplement passer
this
directement au lieu deshared_from_this()
est que cela briserait le mécanisme de propriété:la source
Juste comme note:
CRTP pourrait être utilisé pour implémenter le polymorphisme statique (qui aime le polymorphisme dynamique mais sans table de pointeur de fonction virtuelle).
Le résultat serait:
la source
vtable
s sans utiliser CRTP. Ce quivtable
est vraiment fourni, c'est d'utiliser la classe de base (pointeur ou référence) pour appeler des méthodes dérivées. Vous devriez montrer comment cela se fait avec CRTP ici.Base<>::method ()
n'est même pas appelé, et vous n'utilisez le polymorphisme nulle part.methodImpl
le nommethod
deBase
et dans les classes dérivéesmethodImpl
au lieu demethod