Was ist das merkwürdig wiederkehrende Vorlagenmuster (CRTP)?


187

Kann jemand, ohne sich auf ein Buch zu beziehen, eine gute Erklärung für CRTPein Codebeispiel geben?


2
Lesen Sie CRTP-Fragen zu SO: stackoverflow.com/questions/tagged/crtp . Das könnte Ihnen eine Idee geben.
sbi

68
@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 <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 eine beliebige Klasse zu Aeinem Singleton 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 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<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 <
}

Jetzt können Sie es so verwenden

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

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

Jetzt haben Sie keinen expliziten Operator ==für angegeben Apple? Aber du hast es! Du kannst 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ü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!

CRTP ist eine wunderbare Sache :) HTH


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

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.

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

Ausgabe:

derived foo
AnotherDerived foo

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

[util.smartptr.enab] / 1

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:

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

5

Nur als Hinweis:

CRTP könnte verwendet werden, um statischen Polymorphismus zu implementieren (der dynamischen Polymorphismus mag, jedoch ohne virtuelle Funktionszeigertabelle).

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

Die Ausgabe wäre:

Derived1 method
Derived2 method

1
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"; }};
Barney
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.