2010-11-13 8 views
131

Ohne Bezug auf ein Buch, kann jemand bitte eine gute Erklärung für CRTP mit einem Codebeispiel liefern?Was ist das seltsam wiederkehrende Vorlagenmuster (CRTP)?

+2

lesen CRTP Fragen auf SO: http://stackoverflow.com/questions/tagged/crtp. Das könnte dir eine Idee geben. – sbi

+48

@sbi: Wenn er das tut, wird er seine eigene Frage finden. Und das wäre seltsam wiederkehrend. :) –

+1

Übrigens scheint mir der Begriff "neugierig rekursiv" zu sein. Verkenne ich die Bedeutung? –

Antwort

208

Kurz gesagt, CRTP ist, wenn eine Klasse A eine Basisklasse hat, die eine Template-Spezialisierung für die Klasse A selbst ist. Z.B.

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

Es ist neugierig wiederkehrend, es ist nicht wahr? :)

Nun, was gibt dir das? Dies gibt der X-Vorlage die Möglichkeit, eine Basisklasse für ihre Spezialisierungen zu sein.

Zum Beispiel könnten Sie eine generische Singletonklasse (vereinfachte Version) machen, wie diese

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; 

Um nun ein Singleton eine beliebige Klasse A zu machen, sollten Sie dies tun

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

Nun siehst du? Die Singleton-Vorlage geht davon aus, dass ihre Spezialisierung für jeden Typ X von singleton<X> übernommen wird und somit alle (öffentlichen, geschützten) Mitglieder zugänglich sind, einschließlich der GetInstance! Es gibt andere nützliche Anwendungen von CRTP. Wenn Sie beispielsweise alle Instanzen zählen möchten, die derzeit für Ihre Klasse existieren, diese Logik jedoch in einer separaten Vorlage kapseln möchten (die Idee für eine konkrete Klasse ist ziemlich einfach), haben Sie eine statische Variable, inctors inkrementieren, in dtors dekrementieren). Versuchen Sie es als Übung!

Noch ein nützliches Beispiel für Boost (ich bin mir nicht sicher, wie sie es implementiert haben, aber CRTP wird auch tun). Stellen Sie sich vor, Sie möchten nur den Operator < für Ihre Klassen bereitstellen, aber automatisch den Operator == für sie!

Sie es wie folgt tun könnte:

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, isnit it? 
    Derived const& d2 = static_cast<Derived const&>(op2); 
    return !(d1 < d2) && !(d2 < d1);//assuming derived has operator < 
} 

Jetzt können Sie es wie folgt nun

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

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

verwenden, haben Sie nicht explizit Betreiber zur Verfügung gestellt == für Apfel? Aber du hast es! Sie können schreiben,

int main() 
{ 
    Apple a1; 
    Apple a2; 

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

Dies könnte scheinen, dass Sie weniger schreiben würden, wenn man nur Operator schreibt == für Apple, aber vorstellen, dass die Gleichstellung der Vorlage nicht nur würde == aber>,> =, < = usw. Und Sie könnten diese Definitionen für mehrere Klassen verwenden, den Code wiederverwenden!

CRTP ist eine wunderbare Sache :) HTH

+0

@Armen Tsirunyan: Das ist eine nette Antwort! Danke, dass es hilft :) Im zweiten Beispiel, das Sie angegeben haben, kann die Template-Klasse nur als Basisklasse für CRTP verwendet werden, da die == Annahme. Im Allgemeinen werden in STL Template-Klassen implementiert und speziell für die Verwendung in CRTP markiert oder unabhängig voneinander implementiert, ohne irgendeine Abhängigkeit/Annahme wie im zweiten Beispiel? –

+0

@Als: Nein, STL-Container sind nicht für CRTP konzipiert. Alle ihre Betreiber werden unabhängig bereitgestellt. –

+7

@DeadMG: Ehrlich gesagt, diese Antwort war hilfreich, um das Konzept CRTP zu verstehen, obwohl Singleton nicht die beste Wahl eines Beispiels sein würde, noch die Antwort diente dem Zweck.Da Sie abgelehnt haben, hoffe ich, dass Sie eine bessere Antwort/ein besseres Beispiel finden und das Fenster für Sie offen halten, indem Sie dies nicht als akzeptierte Antwort markieren. –

6

Wie Anmerkung:

CRTP verwendet werden könnte statischen Polymorphismus (die wie dynamische Polymorphie aber ohne virtuelle Funktionszeigertabelle) zu implementieren.

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

würde der Ausgang sein:

Derived1 method 
Derived2 method 
+0

Ich denke, dies würde zusammenbrechen, sobald Sie Base mit einem abgeleiteten verwenden, das auch polymorphe Laufzeit ist, weil der statische Cast des Zeigers auf Basis nicht in einem richtigen Zeiger zu abgeleiteten Ergebnissen führen würde. – odinthenerd

+0

@PorkyBrain: Ich kann es nicht verstehen .. könnten Sie ein Beispiel aus der Praxis geben? – Jichao

+1

Entschuldigung mein schlechter, static_cast kümmert sich um die Änderung. Wenn Sie den Corner Case trotzdem sehen wollen, obwohl er keinen Fehler verursacht, lesen Sie hier: http://ideone.com/LPkktf – odinthenerd

26

Hier können Sie ein gutes Beispiel sehen.Wenn Sie eine virtuelle Methode verwenden, weiß das Programm, was zur Laufzeit ausgeführt wird. Die Implementierung von CRTP ist der Compiler, der in der Kompilierzeit entscheidet !!! Das ist eine großartige Leistung! Diese

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

Konnte man das nicht tun, indem man 'virtual void schreiben (const char * str) const = 0; Obwohl, um fair zu sein, scheint diese Technik sehr hilfreich, wenn 'write' andere Arbeit macht. – atlex2

+7

Mit einer reinen virtuellen Methode lösen Sie die Vererbung in der Laufzeit statt in der Kompilierzeit. CRTP wird verwendet, um dies in der Kompilierzeit zu lösen, damit die Ausführung schneller wird. – GutiMac

2

ist keine direkte Antwort, sondern ein Beispiel dafür, wie CRTP nützlich sein kann.


Ein gutes konkretes Beispiel CRTP iststd::enable_shared_from_this von C++ 11:

[util.smartptr.enab]/1

Eine Klasse von Tenable_­shared_­from_­this<T> erben können die shared_­from_­this Elementfunktionen zu erben, die eine erhalten shared_­ptr Instanz, die auf *this zeigt.

Das heißt, von std::enable_shared_from_this vererben macht es möglich, einen gemeinsamen (oder schwach) Zeiger auf der Instanz, ohne den Zugriff darauf zu erhalten (zum Beispiel von einem Mitglied Funktion, wo man nur über *this wissen).

Es ist nützlich, wenn Sie ein std::shared_ptr geben müssen, aber Sie nur Zugriff auf *this haben:

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

Der Grund, warum Sie nicht nur this direkt statt shared_from_this() passieren kann ist, dass es den Besitz Mechanismus brechen würde:

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

CRTP ist eine Technik zum Implementieren von Kompilierungszeitpolymorphismus. Hier ist ein sehr einfaches Beispiel. Im folgenden Beispiel arbeitet ProcessFoo() mit der Klassenschnittstelle Base und Base::Foo ruft die Methode foo() des abgeleiteten Objekts auf, was Sie mit virtuellen Methoden erreichen wollen.

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

Ausgang:

derived foo 
AnotherDerived foo