Quel est le modèle de modèle curieusement récurrent (CRTP)?


187

Sans faire référence à un livre, quelqu'un peut-il fournir une bonne explication CRTPavec un exemple de code?


2
Lisez les questions CRTP sur SO: stackoverflow.com/questions/tagged/crtp . Cela pourrait vous donner une idée.
sbi

68
@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 <class ActualClass> 
class Singleton
{
   public:
     static ActualClass& GetInstance()
     {
       if(p == nullptr)
         p = new ActualClass;
       return *p; 
     }

   protected:
     static ActualClass* p;
   private:
     Singleton(){}
     Singleton(Singleton const &);
     Singleton& operator = (Singleton const &); 
};
template <class T>
T* Singleton<T>::p = nullptr;

Maintenant, pour faire d'une classe arbitraire Aun singleton, vous devez faire ceci

class A: public Singleton<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<class Derived>
class Equality
{
};

template <class Derived>
bool operator == (Equality<Derived> const& op1, Equality<Derived> const & op2)
{
    Derived const& d1 = static_cast<Derived const&>(op1);//you assume this works     
    //because you know that the dynamic type will actually be your template parameter.
    //wonderful, isn't it?
    Derived const& d2 = static_cast<Derived const&>(op2); 
    return !(d1 < d2) && !(d2 < d1);//assuming derived has operator <
}

Maintenant tu peux l'utiliser comme ça

struct Apple:public Equality<Apple> 
{
    int size;
};

bool operator < (Apple const & a1, Apple const& a2)
{
    return a1.size < a2.size;
}

Maintenant, vous n'avez pas fourni explicitement d'opérateur ==pour Apple? Mais vous l'avez! Tu peux écrire

int main()
{
    Apple a1;
    Apple a2; 

    a1.size = 10;
    a2.size = 10;
    if(a1 == a2) //the compiler won't complain! 
    {
    }
}

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!

CRTP est une chose merveilleuse :) HTH


62
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>
class Writer
{
  public:
    Writer()  { }
    ~Writer()  { }

    void write(const char* str) const
    {
      static_cast<const T*>(this)->writeImpl(str); //here the magic is!!!
    }
};


class FileWriter : public Writer<FileWriter>
{
  public:
    FileWriter(FILE* aFile) { mFile = aFile; }
    ~FileWriter() { fclose(mFile); }

    //here comes the implementation of the write method on the subclass
    void writeImpl(const char* str) const
    {
       fprintf(mFile, "%s\n", str);
    }

  private:
    FILE* mFile;
};


class ConsoleWriter : public Writer<ConsoleWriter>
{
  public:
    ConsoleWriter() { }
    ~ConsoleWriter() { }

    void writeImpl(const char* 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.

http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e

template <typename T>
struct Base {
  void foo() {
    (static_cast<T*>(this))->foo();
  }
};

struct Derived : public Base<Derived> {
  void foo() {
    cout << "derived foo" << endl;
  }
};

struct AnotherDerived : public Base<AnotherDerived> {
  void foo() {
    cout << "AnotherDerived foo" << endl;
  }
};

template<typename T>
void ProcessFoo(Base<T>* b) {
  b->foo();
}


int main()
{
    Derived d1;
    AnotherDerived d2;
    ProcessFoo(&d1);
    ProcessFoo(&d2);
    return 0;
}

Production:

derived foo
AnotherDerived foo

1
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:

[util.smartptr.enab] / 1

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:

struct Node;

void process_node(const std::shared_ptr<Node> &);

struct Node : std::enable_shared_from_this<Node> // CRTP
{
    std::weak_ptr<Node> parent;
    std::vector<std::shared_ptr<Node>> children;

    void add_child(std::shared_ptr<Node> child)
    {
        process_node(shared_from_this()); // Shouldn't pass `this` directly.
        child->parent = weak_from_this(); // Ditto.
        children.push_back(std::move(child));
    }
};

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);

5

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).

#pragma once
#include <iostream>
template <typename T>
class Base
{
    public:
        void method() {
            static_cast<T*>(this)->method();
        }
};

class Derived1 : public Base<Derived1>
{
    public:
        void method() {
            std::cout << "Derived1 method" << std::endl;
        }
};


class Derived2 : public Base<Derived2>
{
    public:
        void method() {
            std::cout << "Derived2 method" << std::endl;
        }
};


#include "crtp.h"
int main()
{
    Derived1 d1;
    Derived2 d2;
    d1.method();
    d2.method();
    return 0;
}

Le résultat serait:

Derived1 method
Derived2 method

1
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"; }};
barney
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.