@sbi: Wenn er das tut, wird er seine eigene Frage finden. Und das würde seltsamerweise immer wieder vorkommen. :)
Craig McQueen
1
Übrigens scheint mir der Begriff "merkwürdig rekursiv" zu sein. Verstehe ich die Bedeutung falsch?
Craig McQueen
1
Craig: Ich denke du bist; es ist "merkwürdig wiederkehrend" in dem Sinne, dass es in mehreren Kontexten auftaucht.
Gareth McCaughan
Antworten:
275
Kurz gesagt, CRTP ist, wenn eine Klasse Aeine Basisklasse hat, die eine Vorlagenspezialisierung für die Klasse Aselbst ist. Z.B
template<class T>class X{...};class A :public X<A>{...};
Es kommt merkwürdigerweise immer wieder vor, nicht wahr? :) :)
Was gibt dir das? Dies gibt der XVorlage tatsächlich die Möglichkeit, eine Basisklasse für ihre Spezialisierungen zu sein.
Sie können beispielsweise eine generische Singleton-Klasse (vereinfachte Version) wie diese erstellen
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;
Um eine beliebige Klasse zu Aeinem Singleton zu machen , sollten Sie dies tun
class A:publicSingleton<A>{//Rest of functionality for class A};
Nun siehst du? Die Singleton-Vorlage geht davon aus, dass ihre Spezialisierung für einen beliebigen Typ Xvon singleton<X>allen (öffentlichen, geschützten) Mitgliedern geerbt wird, 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 vorhanden sind, diese Logik jedoch in einer separaten Vorlage kapseln möchten (die Idee für eine konkrete Klasse ist recht einfach - haben Sie eine statische Variable, Inkrement in ctors, Dekrement in dtors ). Versuchen Sie es als Übung!
Ein weiteres nützliches Beispiel für Boost (ich bin nicht sicher, wie sie es implementiert haben, aber CRTP wird es auch tun). Stellen Sie sich vor, Sie möchten nur einen Operator <für Ihre Klassen bereitstellen , aber automatisch einen Operator ==für sie!
Sie könnten es so machen:
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 <}
Dies könnte scheinen , dass Sie weniger schreiben würde , wenn man nur Operator schrieb ==für Apple, aber vorstellen , dass die EqualityVorlage nicht nur würde , ==sondern >, >=, <=etc. Und Sie können diese Definitionen für verwenden mehrere Klassen, die Wiederverwendung von Code!
Dieser Beitrag befürwortet Singleton nicht als gutes Programmiermuster. Er verwendet es lediglich als Illustration, die allgemein verstanden werden kann.
Imo
3
@Armen: Die Antwort erklärt CRTP auf eine Weise, die klar verstanden werden kann. Es ist eine nette Antwort, danke für diese nette Antwort.
Alok Save
1
@Armen: Danke für diese tolle Erklärung. Ich habe vorher noch nie CRTP bekommen, aber das Gleichheitsbeispiel war aufschlussreich! +1
Paul
1
Ein weiteres Beispiel für die Verwendung von CRTP ist, wenn Sie eine nicht kopierbare Klasse benötigen: template <class T> -Klasse NonCopyable {protected: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); Nicht kopierbar & operator = (const Nicht kopierbar &); }; Dann verwenden Sie noncopyable wie folgt: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren
2
@ Welpe: Singleton ist nicht schrecklich. Es wird von unterdurchschnittlichen Programmierern bei weitem überbeansprucht, wenn andere Ansätze angemessener wären, aber dass die meisten seiner Verwendungen schrecklich sind, macht das Muster selbst nicht schrecklich. Es gibt Fälle, in denen Singleton die beste Option ist, obwohl diese selten sind.
Kaiserludi
47
Hier sehen Sie ein gutes Beispiel. Wenn Sie eine virtuelle Methode verwenden, weiß das Programm, was zur Laufzeit ausgeführt wird. Bei der Implementierung von CRTP entscheidet der Compiler in der Kompilierungszeit !!! Das ist eine großartige Leistung!
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);}};
Könnten Sie dies nicht durch Definieren tun virtual void write(const char* str) const = 0;? Um fair zu sein, scheint diese Technik bei writeanderen Arbeiten sehr hilfreich zu sein .
Atlex2
24
Mit einer rein virtuellen Methode lösen Sie die Vererbung zur Laufzeit anstatt zur Kompilierungszeit. CRTP wird verwendet, um dies in der Kompilierungszeit zu lösen, sodass die Ausführung schneller erfolgt.
GutiMac
1
Versuchen Sie, eine einfache Funktion zu erstellen, die einen abstrakten Writer erwartet: Sie können dies nicht tun, da es nirgendwo eine Klasse mit dem Namen Writer gibt. Wo ist also genau Ihr Polymorphismus? Dies ist überhaupt nicht gleichbedeutend mit virtuellen Funktionen und weitaus weniger nützlich.
22
CRTP ist eine Technik zur Implementierung des Polymorphismus zur Kompilierungszeit. Hier ist ein sehr einfaches Beispiel. Im folgenden Beispiel ProcessFoo()wird mit der BaseKlassenschnittstelle gearbeitet und Base::Foodie foo()Methode des abgeleiteten Objekts aufgerufen. Dies ist das Ziel, das Sie mit virtuellen Methoden erreichen möchten.
In diesem Beispiel kann es sich auch lohnen, ein Beispiel für die Implementierung eines Standard-foo () in der Basisklasse hinzuzufügen, das aufgerufen wird, wenn kein Derived es implementiert hat. AKA ändere foo in der Base in einen anderen Namen (zB caller ()), füge der Base eine neue Funktion foo () hinzu, die die "Base" des Couts enthält. Rufen Sie dann caller () innerhalb von ProcessFoo an
wizurd
@wizurd Dieses Beispiel soll eher eine reine virtuelle Basisklassenfunktion veranschaulichen, dh wir erzwingen, dass foo()sie von der abgeleiteten Klasse implementiert wird.
Blueskin
3
Dies ist meine Lieblingsantwort, da sie auch zeigt, warum dieses Muster für die ProcessFoo()Funktion nützlich ist.
Pietro
Ich verstehe den Sinn dieses Codes nicht, weil er mit void ProcessFoo(T* b)und ohne Derived und AnotherDerived immer noch funktionieren würde. IMHO wäre es interessanter, wenn ProcessFoo keine Vorlagen irgendwie verwenden würde.
Gabriel Devillers
1
@GabrielDevillers Erstens funktioniert das Templatized ProcessFoo()mit jedem Typ, der die Schnittstelle implementiert, dh in diesem Fall sollte der Eingabetyp T eine Methode namens haben foo(). Zweitens ProcessFoowürden Sie wahrscheinlich RTTI verwenden, um eine Nicht-Vorlage für die Arbeit mit mehreren Typen zu erhalten, was wir vermeiden möchten. Darüber hinaus bietet die Vorlagenversion eine Überprüfung der Kompilierungszeit auf der Benutzeroberfläche.
Blueskin
6
Dies ist keine direkte Antwort, sondern ein Beispiel dafür, wie nützlich CRTP sein kann.
Ein gutes konkretes Beispiel für CRTP ist std::enable_shared_from_thisC ++ 11:
Eine Klasse Tkann von erben enable_shared_from_this<T>, um die Elementfunktionen zu erben shared_from_this, auf die eine shared_ptrInstanz verweist, auf die verwiesen wird *this.
Das heißt, das Erben von std::enable_shared_from_thisermöglicht es, einen gemeinsam genutzten (oder schwachen) Zeiger auf Ihre Instanz zu erhalten, ohne darauf zugreifen zu können (z. B. von einer Mitgliedsfunktion, von der Sie nur wissen *this).
Es ist nützlich, wenn Sie eine geben müssen, std::shared_ptraber nur Zugriff auf *this:
Der Grund, warum Sie nicht einfach thisdirekt passieren können, shared_from_this()ist, dass dies den Eigentumsmechanismus 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);
CRTP könnte verwendet werden, um statischen Polymorphismus zu implementieren (der dynamischen Polymorphismus mag, jedoch ohne virtuelle Funktionszeigertabelle).
Entschuldigung, mein schlechter, static_cast kümmert sich um die Änderung. Wenn Sie den Eckfall
odinthenerd
30
Schlechtes Beispiel. Dieser Code könnte vtableohne CRTP ohne s ausgeführt werden. Was vtablewirklich bietet, ist die Verwendung der Basisklasse (Zeiger oder Referenz) zum Aufrufen abgeleiteter Methoden. Hier sollten Sie zeigen, wie es mit CRTP gemacht wird.
Etherealone
17
In Ihrem Beispiel wird Base<>::method ()es nicht einmal aufgerufen, und Sie verwenden nirgendwo Polymorphismus.
MikeMB
1
@Jichao, laut @MikeMBs Anmerkung, sollten Sie methodImplden Namen methodvon Baseund in abgeleiteten Klassen methodImplanstelle vonmethod
Ivan Kush
1
Wenn Sie eine ähnliche Methode () verwenden, ist sie statisch gebunden und Sie benötigen keine gemeinsame Basisklasse. Denn sowieso konnte man es nicht polymorph durch Basisklassenzeiger oder ref verwenden. Der Code sollte also folgendermaßen aussehen: #include <iostream> template <typname 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"; }};
Antworten:
Kurz gesagt, CRTP ist, wenn eine Klasse
A
eine Basisklasse hat, die eine Vorlagenspezialisierung für die KlasseA
selbst ist. Z.BEs kommt merkwürdigerweise immer wieder vor, nicht wahr? :) :)
Was gibt dir das? Dies gibt der
X
Vorlage tatsächlich die Möglichkeit, eine Basisklasse für ihre Spezialisierungen zu sein.Sie können beispielsweise eine generische Singleton-Klasse (vereinfachte Version) wie diese erstellen
Um eine beliebige Klasse zu
A
einem Singleton zu machen , sollten Sie dies tunNun siehst du? Die Singleton-Vorlage geht davon aus, dass ihre Spezialisierung für einen beliebigen Typ
X
vonsingleton<X>
allen (öffentlichen, geschützten) Mitgliedern geerbt wird, einschließlich derGetInstance
! Es gibt andere nützliche Anwendungen von CRTP. Wenn Sie beispielsweise alle Instanzen zählen möchten, die derzeit für Ihre Klasse vorhanden sind, diese Logik jedoch in einer separaten Vorlage kapseln möchten (die Idee für eine konkrete Klasse ist recht einfach - haben Sie eine statische Variable, Inkrement in ctors, Dekrement in dtors ). Versuchen Sie es als Übung!Ein weiteres nützliches Beispiel für Boost (ich bin nicht sicher, wie sie es implementiert haben, aber CRTP wird es auch tun). Stellen Sie sich vor, Sie möchten nur einen Operator
<
für Ihre Klassen bereitstellen , aber automatisch einen Operator==
für sie!Sie könnten es so machen:
Jetzt können Sie es so verwenden
Jetzt haben Sie keinen expliziten Operator
==
für angegebenApple
? Aber du hast es! Du kannst schreibenDies könnte scheinen , dass Sie weniger schreiben würde , wenn man nur Operator schrieb
==
fürApple
, aber vorstellen , dass dieEquality
Vorlage nicht nur würde ,==
sondern>
,>=
,<=
etc. Und Sie können diese Definitionen für verwenden mehrere Klassen, die Wiederverwendung von Code!CRTP ist eine wunderbare Sache :) HTH
quelle
Hier sehen Sie ein gutes Beispiel. Wenn Sie eine virtuelle Methode verwenden, weiß das Programm, was zur Laufzeit ausgeführt wird. Bei der Implementierung von CRTP entscheidet der Compiler in der Kompilierungszeit !!! Das ist eine großartige Leistung!
quelle
virtual void write(const char* str) const = 0;
? Um fair zu sein, scheint diese Technik beiwrite
anderen Arbeiten sehr hilfreich zu sein .CRTP ist eine Technik zur Implementierung des Polymorphismus zur Kompilierungszeit. Hier ist ein sehr einfaches Beispiel. Im folgenden Beispiel
ProcessFoo()
wird mit derBase
Klassenschnittstelle gearbeitet undBase::Foo
diefoo()
Methode des abgeleiteten Objekts aufgerufen. Dies ist das Ziel, das Sie mit virtuellen Methoden erreichen möchten.http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e
Ausgabe:
quelle
foo()
sie von der abgeleiteten Klasse implementiert wird.ProcessFoo()
Funktion nützlich ist.void ProcessFoo(T* b)
und ohne Derived und AnotherDerived immer noch funktionieren würde. IMHO wäre es interessanter, wenn ProcessFoo keine Vorlagen irgendwie verwenden würde.ProcessFoo()
mit jedem Typ, der die Schnittstelle implementiert, dh in diesem Fall sollte der Eingabetyp T eine Methode namens habenfoo()
. ZweitensProcessFoo
würden Sie wahrscheinlich RTTI verwenden, um eine Nicht-Vorlage für die Arbeit mit mehreren Typen zu erhalten, was wir vermeiden möchten. Darüber hinaus bietet die Vorlagenversion eine Überprüfung der Kompilierungszeit auf der Benutzeroberfläche.Dies ist keine direkte Antwort, sondern ein Beispiel dafür, wie nützlich CRTP sein kann.
Ein gutes konkretes Beispiel für CRTP ist
std::enable_shared_from_this
C ++ 11:Das heißt, das Erben von
std::enable_shared_from_this
ermöglicht es, einen gemeinsam genutzten (oder schwachen) Zeiger auf Ihre Instanz zu erhalten, ohne darauf zugreifen zu können (z. B. von einer Mitgliedsfunktion, von der Sie nur wissen*this
).Es ist nützlich, wenn Sie eine geben müssen,
std::shared_ptr
aber nur Zugriff auf*this
:Der Grund, warum Sie nicht einfach
this
direkt passieren können,shared_from_this()
ist, dass dies den Eigentumsmechanismus brechen würde:quelle
Nur als Hinweis:
CRTP könnte verwendet werden, um statischen Polymorphismus zu implementieren (der dynamischen Polymorphismus mag, jedoch ohne virtuelle Funktionszeigertabelle).
Die Ausgabe wäre:
quelle
vtable
ohne CRTP ohne s ausgeführt werden. Wasvtable
wirklich bietet, ist die Verwendung der Basisklasse (Zeiger oder Referenz) zum Aufrufen abgeleiteter Methoden. Hier sollten Sie zeigen, wie es mit CRTP gemacht wird.Base<>::method ()
es nicht einmal aufgerufen, und Sie verwenden nirgendwo Polymorphismus.methodImpl
den Namenmethod
vonBase
und in abgeleiteten KlassenmethodImpl
anstelle vonmethod