Qual è il modello di modello curiosamente ricorrente (CRTP)?


187

Senza fare riferimento a un libro, qualcuno può fornire una buona spiegazione CRTPcon un esempio di codice?


2
Leggi le domande CRTP su SO: stackoverflow.com/questions/tagged/crtp . Questo potrebbe darti un'idea.
sabato

68
@sbi: Se lo fa, troverà la sua domanda. E questo sarebbe curiosamente ricorrente. :)
Craig McQueen l'

1
A proposito, mi sembra che il termine dovrebbe essere "curiosamente ricorrente". Sto fraintendendo il significato?
Craig McQueen,

1
Craig: Penso che tu lo sia; è "curiosamente ricorrente", nel senso che è stato scoperto affiorare in più contesti.
Gareth McCaughan,

Risposte:


276

In breve, CRTP è quando una classe Aha una classe base che è una specializzazione di modello per la classe Astessa. Per esempio

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

Lo è curiosamente ripresentando, non è vero? :)

Ora, cosa ti dà questo? Questo in realtà dà ilX modello la possibilità di essere una classe base per le sue specializzazioni.

Ad esempio, potresti creare una classe singleton generica (versione semplificata) come questa

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;

Ora, per rendere una classe arbitraria Aun singleton dovresti farlo

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

Come vedi? Il modello singleton presuppone che la sua specializzazione per qualsiasi tipo Xverrà ereditata singleton<X>e quindi tutti i suoi membri (pubblici, protetti) saranno accessibili, incluso il GetInstance! Esistono altri usi utili di CRTP. Ad esempio, se si desidera contare tutte le istanze attualmente esistenti per la propria classe, ma si desidera incapsulare questa logica in un modello separato (l'idea di una classe concreta è abbastanza semplice: disporre di una variabile statica, incremento in dottori, decremento in dottori ). Prova a farlo come esercizio!

Ancora un altro esempio utile, per Boost (non sono sicuro di come lo abbiano implementato, ma lo farà anche CRTP). Immagina di voler fornire solo un operatore <per le tue classi, ma automaticamente un operatore ==per loro!

potresti farlo in questo modo:

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 <
}

Ora puoi usarlo in questo modo

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

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

Ora, non hai fornito esplicitamente operatore ==per Apple? Ma ce l'hai! Tu puoi scrivere

int main()
{
    Apple a1;
    Apple a2; 

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

Questo potrebbe sembrare che si può scrivere di meno se appena scritto operatore ==per Apple, ma immaginare che il Equalitymodello avrebbe fornito non solo ==, ma >, >=, <=ecc E si potrebbe usare queste definizioni per più classi, riutilizzando il codice!

CRTP è una cosa meravigliosa :) HTH


62
Questo post non sostiene singleton come un buon modello di programmazione. Lo usa semplicemente come un'illustrazione che può essere comunemente compresa.imo the-1 è ingiustificato
John Dibling

3
@Armen: la risposta spiega CRTP in un modo che può essere compreso chiaramente, è una bella risposta, grazie per una risposta così bella.
Risparmia il

1
@Armen: grazie per questa grande spiegazione. Prima non stavo ottenendo CRTP, ma l'esempio di uguaglianza è stato illuminante! +1
Paul

1
Ancora un altro esempio dell'uso di CRTP è quando hai bisogno di una classe non copiabile: template <class T> class NonCopyable {protected: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); NonCopyable & operator = (const NonCopyable &); }; Quindi si utilizza non copiabile come di seguito: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren,

2
@Puppy: Singleton non è terribile. È di gran lunga abusato da programmatori al di sotto della media quando altri approcci sarebbero più appropriati, ma che la maggior parte dei suoi usi sono terribili non rende il modello stesso terribile. Ci sono casi in cui singleton è l'opzione migliore, anche se quelli sono rari.
Kaiserludi,

47

Qui puoi vedere un ottimo esempio. Se si utilizza il metodo virtuale, il programma saprà cosa eseguire in fase di esecuzione. L'implementazione di CRTP è il compilatore che decide in tempo di compilazione !!! Questa è una grande esibizione!

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

Non potresti farlo definendo virtual void write(const char* str) const = 0;? Sebbene sia corretta, questa tecnica sembra molto utile quando writesi svolgono altri lavori.
atlex2

26
Usando un metodo virtuale puro stai risolvendo l'ereditarietà in fase di esecuzione anziché in fase di compilazione. CRTP viene utilizzato per risolvere questo problema in tempo di compilazione, quindi l'esecuzione sarà più veloce.
GutiMac,

1
Prova a fare una semplice funzione che si aspetta uno scrittore astratto: non puoi farlo perché non esiste una classe chiamata Writer da nessuna parte, quindi dov'è esattamente il tuo polimorfismo? Questo non è affatto equivalente alle funzioni virtuali ed è molto meno utile.

22

CRTP è una tecnica per implementare il polimorfismo in fase di compilazione. Ecco un esempio molto semplice. Nell'esempio seguente, ProcessFoo()lavora con l' Baseinterfaccia di classe e Base::Foorichiama il foo()metodo dell'oggetto derivato , che è ciò che intendi fare con i metodi virtuali.

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

Produzione:

derived foo
AnotherDerived foo

1
Potrebbe anche valere la pena in questo esempio aggiungere un esempio di come implementare un foo () predefinito nella classe Base che verrà chiamato se nessun Derivato lo ha implementato. AKA cambia foo nella Base con un altro nome (es. Caller ()), aggiungi una nuova funzione foo () alla Base che dice "Base". Quindi chiama caller () all'interno di ProcessFoo
wizurd l'

@wizurd Questo esempio è più per illustrare una funzione di classe di base virtuale pura, vale a dire che applichiamo che foo()è implementato dalla classe derivata.
blueskin,

3
Questa è la mia risposta preferita, poiché mostra anche perché questo modello è utile con la ProcessFoo()funzione.
Pietro,

Non capisco il punto di questo codice, perché con void ProcessFoo(T* b)e senza avere Derived e AnotherDerived effettivamente funzionato funzionerebbe comunque. IMHO sarebbe più interessante se ProcessFoo non usasse i template in qualche modo.
Gabriel Devillers,

1
@GabrielDevillers In primo luogo, il modello ProcessFoo()funzionerà con qualsiasi tipo che implementa l'interfaccia, ovvero in questo caso il tipo di input T dovrebbe avere un metodo chiamato foo(). In secondo luogo, al fine di ottenere un non templatized ProcessFooper lavorare con più tipi, probabilmente finiresti per usare RTTI, che è ciò che vogliamo evitare. Inoltre, la versione templatizzata fornisce il controllo del tempo di compilazione sull'interfaccia.
blueskin,

6

Questa non è una risposta diretta, ma piuttosto un esempio di come CRTP può essere utile.


Un buon esempio concreto di CRTP è std::enable_shared_from_thisda C ++ 11:

[Util.smartptr.enab] / 1

Una classe Tpuò ereditare da enable_­shared_­from_­this<T>per ereditare le shared_­from_­thisfunzioni membro che ottengono shared_­ptrun'istanza che punta a *this.

Cioè, ereditare da std::enable_shared_from_thisconsente di ottenere un puntatore condiviso (o debole) alla propria istanza senza accedervi (ad es. Da una funzione membro di cui si conosce solo *this).

È utile quando devi dare un std::shared_ptrma hai accesso solo a *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));
    }
};

Il motivo per cui non puoi semplicemente passare thisdirettamente invece shared_from_this()è che si romperà il meccanismo di proprietà:

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

Proprio come nota:

Il CRTP potrebbe essere usato per implementare il polimorfismo statico (che come il polimorfismo dinamico ma senza una tabella di puntatori di funzioni virtuali).

#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;
}

L'output sarebbe:

Derived1 method
Derived2 method

1
mi dispiace mio cattivo, static_cast si occupa del cambiamento. Se vuoi comunque vedere la custodia ad angolo anche se non causa errori, vedi qui: ideone.com/LPkktf
odinthenerd

30
Cattivo esempio Questo codice potrebbe essere eseguito senza messaggi vtablesenza utilizzare CRTP. Ciò vtableche realmente fornisce è l'uso della classe base (puntatore o riferimento) per chiamare metodi derivati. Qui dovresti mostrare come si fa con CRTP.
Etherealone,

17
Nel tuo esempio, Base<>::method ()non viene nemmeno chiamato, né usi il polimorfismo da nessuna parte.
MikeMB,

1
@Jichao, secondo la nota di @MikeMB, dovresti chiamare methodImplil nome methoddi Basee in classi derivate methodImplinvece dimethod
Ivan Kush,

1
se usi un metodo simile (), allora è associato staticamente e non hai bisogno della classe base comune. Perché comunque non è possibile usarlo polimorficamente tramite il puntatore della classe base o il riferimento. Quindi il codice dovrebbe apparire così: #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,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.