@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<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;
Teraz, aby uczynić arbitralną klasę Asinglem, powinieneś to zrobić
class A:publicSingleton<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<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 <}
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!
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>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);}};
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.
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:
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:
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);
CRTP może być wykorzystany do implementacji statycznego polimorfizmu (który lubi dynamiczny polimorfizm, ale bez tablicy wskaźników funkcji wirtualnej).
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"; }};
Odpowiedzi:
W skrócie, CRTP ma miejsce, gdy klasa
A
ma klasę podstawową, która jest specjalizacją szablonu dlaA
samej klasy . Na przykładTo jest ciekawie powtarzające się, prawda? :)
Co ci to daje? Daje to
X
szablonowi możliwość bycia klasą bazową dla jego specjalizacji.Na przykład, możesz stworzyć ogólną klasę singleton (wersja uproszczona) w ten sposób
Teraz, aby uczynić arbitralną klasę
A
singlem, powinieneś to zrobićWięc widzisz? Szablon singletonu zakłada, że jego specjalizacja dla dowolnego typu
X
zostanie odziedziczona,singleton<X>
a zatem wszystkie jego (publiczne, chronione) elementy będą dostępne, w tymGetInstance
! 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:
Teraz możesz używać tego w ten sposób
Teraz nie przewidują explicite operatora
==
zaApple
? Ale ty to masz! Możesz pisaćTo może wydawać się, że można napisać mniej, jeśli tylko napisali operatora
==
zaApple
, ale wyobraźmy sobie, żeEquality
szablon 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
źródło
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!
źródło
virtual void write(const char* str) const = 0;
? Chociaż, mówiąc uczciwie, technika ta wydaje się bardzo pomocna, gdywrite
wykonuje inne prace.CRTP to technika implementacji polimorfizmu w czasie kompilacji. Oto bardzo prosty przykład. W poniższym przykładzie
ProcessFoo()
pracuje zBase
interfejsem klasy iBase::Foo
wywołujefoo()
metodę obiektu pochodnego , do czego dąży się metodami wirtualnymi.http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e
Wynik:
źródło
foo()
jest implementowana przez klasę pochodną.ProcessFoo()
funkcji.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.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 nazwiefoo()
. Po drugie, aby uzyskać szablon bezProcessFoo
pracy z wieloma typami, prawdopodobnie użyłbyś RTTI, a tego chcemy uniknąć. Co więcej, wersja szablonowa zapewnia kontrolę czasu kompilacji interfejsu.To nie jest bezpośrednia odpowiedź, ale raczej przykład przydatności CRTP .
Dobry konkretny przykład CRTP pochodzi
std::enable_shared_from_this
z C ++ 11:Oznacza to, że dziedziczenie po
std::enable_shared_from_this
umoż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_ptr
ale masz dostęp tylko do*this
:Powodem, dla którego nie można po prostu przekazać
this
bezpośrednio,shared_from_this()
jest to, że zepsułoby to mechanizm własności:źródło
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).
Dane wyjściowe będą:
źródło
vtable
żadnych znaków bez użycia CRTP. To,vtable
co 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.Base<>::method ()
nie jest nawet wywoływany ani nigdzie nie używasz polimorfizmu.methodImpl
wmethod
zBase
i w klasach pochodnych wymienićmethodImpl
zamiastmethod