web-dev-qa-db-fra.com

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

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

166
Alok Save

En bref, CRTP est le cas où une classe A a une classe de base, qui est une spécialisation de modèle pour la classe A elle-même. Par exemple.

template <class T> 
class X{...};
class A : public X<A> {...};

C'est est curieusement récurrent, n'est-ce pas? :)

Maintenant, qu'est-ce que cela vous donne? Cela donne en fait au modèle X 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, afin de faire une classe arbitraire A un singleton, vous devriez le faire

class A: public Singleton<A>
{
   //Rest of functionality for class A
};

Donc tu vois? Le modèle singleton suppose que sa spécialisation pour tout type X sera héritée de singleton<X> et tous ses membres (publics, protégés) seront accessibles, y compris le GetInstance! Il existe d'autres utilisations utiles du CRTP. Par exemple, si vous souhaitez compter toutes les instances existantes dans votre classe, mais que vous souhaitez 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 dans ctors, décrémenter dans dtors ). Essayez de le faire comme un exercice!

Encore un autre exemple utile, pour Boost (je ne sais pas comment ils l’ont mis en œuvre, mais le CRTP le fera aussi). Imaginez que vous souhaitiez fournir uniquement l'opérateur < pour vos cours mais automatiquement opérateur == pour eux!

vous pourriez 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, vous pouvez 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 explicitement fourni l'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 imaginons que le modèle Equality fournisse non seulement == mais >, >=, <= etc. Et vous pourriez utiliser ces définitions pour plusieurs classes, en réutilisant le code!

Le CRTP est une chose merveilleuse :) HTH

259
Armen Tsirunyan

Ici vous pouvez voir un bon exemple. Si vous utilisez une méthode virtuelle, le programme saura ce qui sera exécuté au moment de l'exécution. L'implémentation de CRTP est le compilateur qui décide en 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);
    }
};
41
GutiMac

Le CRTP est une technique permettant de mettre en œuvre un polymorphisme au moment de la compilation. Voici un exemple très simple. Dans l'exemple ci-dessous, ProcessFoo() travaille avec Base interface de classe et Base::Foo Invoque la méthode foo() de l'objet dérivé, ce que vous souhaitez faire. avec des 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;
}

Sortie:

derived foo
AnotherDerived foo
18
blueskin

Juste comme note:

CRTP pourrait être utilisé pour implémenter un polymorphisme statique (qui ressemble au 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;
}

La sortie serait:

Derived1 method
Derived2 method
5
Jichao

Ce n'est pas une réponse directe, mais plutôt un exemple de la façon dont [~ # ~] crtp [~ # ~] peut être utile.


Un bon exemple concret de [~ # ~] crtp [~ # ~] est std::enable_shared_from_this De C++ 11:

[util.smartptr.enab]/1

Une classe T peut hériter de enable_­shared_­from_­this<T> Pour hériter des fonctions membres shared_­from_­this Qui obtiennent une instance shared_­ptr Pointant vers *this.

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 où vous ne connaissez que *this).

C'est utile lorsque vous devez donner un std::shared_ptr Mais vous avez seulement accès à *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 this directement à la place 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);
4
Mário Feroldi