Co to jest ciekawie powtarzający się wzorzec szablonu (CRTP)?


187

Czy bez odwoływania się do książki ktoś mógłby podać dobre wyjaśnienie na CRTPprzykładzie kodu?


2
Przeczytaj pytania CRTP na SO: stackoverflow.com/questions/tagged/crtp . To może dać ci pewien pomysł.
sbi

68
@sbi: Jeśli to zrobi, znajdzie swoje własne pytanie. I to byłoby dziwnie powtarzające się. :)
Craig McQueen

1
BTW, wydaje mi się, że termin ten powinien być „ciekawie powracający”. Czy nie rozumiem znaczenia?
Craig McQueen

1
Craig: Myślę, że jesteś; „ciekawie powraca” w tym sensie, że pojawiło się w wielu kontekstach.
Gareth McCaughan,

Odpowiedzi:


275

W skrócie, CRTP ma miejsce, gdy klasa Ama klasę podstawową, która jest specjalizacją szablonu dla Asamej klasy . Na przykład

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

To jest ciekawie powtarzające się, prawda? :)

Co ci to daje? Daje to Xszablonowi możliwość bycia klasą bazową dla jego specjalizacji.

Na przykład, możesz stworzyć ogólną klasę singleton (wersja uproszczona) w ten sposób

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;

Teraz, aby uczynić arbitralną klasę Asinglem, powinieneś to zrobić

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

Więc widzisz? Szablon singletonu zakłada, że ​​jego specjalizacja dla dowolnego typu Xzostanie odziedziczona, singleton<X>a zatem wszystkie jego (publiczne, chronione) elementy będą dostępne, w tym GetInstance! Istnieją inne przydatne zastosowania CRTP. Na przykład, jeśli chcesz policzyć wszystkie instancje, które obecnie istnieją dla twojej klasy, ale chcesz zawrzeć tę logikę w osobnym szablonie (pomysł na konkretną klasę jest dość prosty - mieć zmienną statyczną, przyrost w ctre, spadek w dtr ). Spróbuj to zrobić jako ćwiczenie!

Kolejny przydatny przykład dla Boost (nie jestem pewien, jak go zaimplementowali, ale CRTP też to zrobi). Wyobraź sobie, że chcesz zapewnić tylko operator <dla swoich klas, ale automatycznie ==dla nich!

możesz to zrobić w następujący sposób:

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

Teraz możesz używać tego w ten sposób

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

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

Teraz nie przewidują explicite operatora ==za Apple? Ale ty to masz! Możesz pisać

int main()
{
    Apple a1;
    Apple a2; 

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

To może wydawać się, że można napisać mniej, jeśli tylko napisali operatora ==za Apple, ale wyobraźmy sobie, że Equalityszablon będzie nie tylko zapewniają ==jednak >, >=, <=itd. I można użyć tych definicji dla wielu klas, ponowne użycie kodu!

CRTP to cudowna rzecz :) HTH


61
Ten post nie opowiada się za singletonem jako dobrym wzorcem programistycznym. Po prostu wykorzystuje go jako ilustrację, która może być powszechnie rozumiana. I-1 jest nieuzasadniony
John Dibling

3
@Armen: Odpowiedź wyjaśnia CRTP w sposób, który można zrozumieć, jest to ładna odpowiedź, dzięki za tak miłą odpowiedź.
Alok Zapisz

1
@Armen: dzięki za to wspaniałe wyjaśnienie. Wcześniej dostawałem CRTP, ale przykład równości jest pouczający! +1
Paul

1
Jeszcze innym przykładem użycia CRTP jest sytuacja, gdy potrzebujesz klasy, której nie można skopiować: szablon <klasa T> klasa NonCopyable {chroniony: NonCopyable () {} ~ NonCopyable () {} prywatny: NonCopyable (const NonCopyable &); NonCopyable & operator = (const NonCopyable &); }; Następnie użyjesz opcji noncopyable jak poniżej: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren

2
@Puppy: Singleton nie jest straszny. Jest zdecydowanie nadużywany przez programistów poniżej średniej, gdy inne podejścia byłyby bardziej odpowiednie, ale to, że większość jego zastosowań jest okropna, nie czyni samego schematu okropnym. Są przypadki, w których singleton jest najlepszą opcją, chociaż są one rzadkie.
Kaiserludi

47

Tutaj możesz zobaczyć świetny przykład. Jeśli użyjesz metody wirtualnej, program będzie wiedział, co wykonać w czasie wykonywania. Implementując CRTP, kompilator decyduje o czasie kompilacji !!! To świetny występ!

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

Nie możesz tego zrobić, definiując virtual void write(const char* str) const = 0;? Chociaż, mówiąc uczciwie, technika ta wydaje się bardzo pomocna, gdy writewykonuje inne prace.
atlex2,

26
Za pomocą czystej metody wirtualnej rozwiązujesz dziedziczenie w środowisku wykonawczym zamiast w czasie kompilacji. CRTP służy do rozwiązania tego w czasie kompilacji, dzięki czemu wykonanie będzie szybsze.
GutiMac,

1
Spróbuj stworzyć prostą funkcję, która oczekuje abstrakcyjnego Writera: nie możesz tego zrobić, ponieważ nigdzie nie ma klasy Writer, więc gdzie dokładnie jest twój polimorfizm? Nie jest to wcale równoważne z funkcjami wirtualnymi i jest o wiele mniej przydatne.

22

CRTP to technika implementacji polimorfizmu w czasie kompilacji. Oto bardzo prosty przykład. W poniższym przykładzie ProcessFoo()pracuje z Baseinterfejsem klasy i Base::Foowywołuje foo()metodę obiektu pochodnego , do czego dąży się metodami wirtualnymi.

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

Wynik:

derived foo
AnotherDerived foo

1
Warto również w tym przykładzie dodać przykład, w jaki sposób zaimplementować domyślną funkcję foo () w klasie Base, która zostanie wywołana, jeśli nie został zaimplementowany przez Derived. AKA zmień foo w Base na inną nazwę (np. Caller ()), dodaj nową funkcję foo () do Base, która wykrywa „Base”. Następnie wywołaj caller () w
ProcessFoo

@wizurd Ten przykład jest bardziej zilustrowany czystą wirtualną funkcją klasy bazowej, tzn. egzekwujemy, która foo()jest implementowana przez klasę pochodną.
blueskin

3
To moja ulubiona odpowiedź, ponieważ pokazuje również, dlaczego ten wzór jest użyteczny w ProcessFoo()funkcji.
Pietro,

Nie rozumiem, o co chodzi z tym kodem, ponieważ void ProcessFoo(T* b)niezależnie od tego, czy rzeczywiście wyprowadziłem Derived i AnotherDerived, nadal działałoby. IMHO byłoby bardziej interesujące, gdyby ProcessFoo w jakiś sposób nie korzystało z szablonów.
Gabriel Devillers

1
@GabrielDevillers Po pierwsze, szablon ProcessFoo()będzie działał z każdym typem, który implementuje interfejs, tj. W tym przypadku typ wejściowy T powinien mieć metodę o nazwie foo(). Po drugie, aby uzyskać szablon bez ProcessFoopracy z wieloma typami, prawdopodobnie użyłbyś RTTI, a tego chcemy uniknąć. Co więcej, wersja szablonowa zapewnia kontrolę czasu kompilacji interfejsu.
blueskin

6

To nie jest bezpośrednia odpowiedź, ale raczej przykład przydatności CRTP .


Dobry konkretny przykład CRTP pochodzi std::enable_shared_from_thisz C ++ 11:

[util.smartptr.enab] / 1

Klasa Tmoże dziedziczyć funkcje enable_­shared_­from_­this<T>dziedziczące, shared_­from_­thisktóre uzyskują shared_­ptrinstancję wskazującą *this.

Oznacza to, że dziedziczenie po std::enable_shared_from_thisumożliwia uzyskanie udostępnionego (lub słabego) wskaźnika do instancji bez dostępu do niego (np. Z funkcji członka, o której wiesz tylko *this).

Jest to przydatne, gdy musisz podać, std::shared_ptrale masz dostęp tylko do *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));
    }
};

Powodem, dla którego nie można po prostu przekazać thisbezpośrednio, shared_from_this()jest to, że zepsułoby to mechanizm własności:

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

Podobnie jak uwaga:

CRTP może być wykorzystany do implementacji statycznego polimorfizmu (który lubi dynamiczny polimorfizm, ale bez tablicy wskaźników funkcji wirtualnej).

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

Dane wyjściowe będą:

Derived1 method
Derived2 method

1
przepraszam mój zły, static_cast zajmuje się zmianą. Jeśli mimo wszystko chcesz zobaczyć obudowę narożną, mimo że nie powoduje ona błędu, zobacz tutaj: ideone.com/LPkktf
odinthenerd

30
Zły przykład. Ten kod można wykonać bez vtableżadnych znaków bez użycia CRTP. To, vtableco naprawdę zapewnia, to używanie klasy bazowej (wskaźnika lub odwołania) do wywoływania metod pochodnych. W tym miejscu powinieneś pokazać, jak to się robi z CRTP.
Etherealone

17
W twoim przykładzie Base<>::method ()nie jest nawet wywoływany ani nigdzie nie używasz polimorfizmu.
MikeMB

1
@Jichao według @MikeMB Notatki, należy zadzwonić methodImplw methodz Basei w klasach pochodnych wymienić methodImplzamiastmethod
Ivan Kush

1
jeśli użyjesz podobnej metody (), to jest ona związana statycznie i nie potrzebujesz wspólnej klasy bazowej. Ponieważ w każdym razie nie można go użyć polimorficznie przez wskaźnik klasy bazowej lub ref. Kod powinien więc wyglądać następująco: #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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.