@sbi: Si hace eso, encontrará su propia pregunta. Y eso sería curiosamente recurrente. :)
Craig McQueen
1
Por cierto, me parece que el término debería ser "curiosamente recurrente". ¿Estoy malinterpretando el significado?
Craig McQueen
1
Craig: creo que sí; es "curiosamente recurrente" en el sentido de que se descubrió que surgía en múltiples contextos.
Gareth McCaughan
Respuestas:
276
En resumen, CRTP es cuando una clase Atiene una clase base que es una especialización de plantilla para la clase Amisma. P.ej
template<class T>class X{...};class A :public X<A>{...};
Se está curiosamente recurrente, no es así? :)
Ahora, ¿qué te da esto? Esto realmente le da a la Xplantilla la capacidad de ser una clase base para sus especializaciones.
Por ejemplo, podría hacer una clase singleton genérica (versión simplificada) como esta
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;
Ahora, para que una clase arbitraria sea Aun singleton, debes hacer esto
class A:publicSingleton<A>{//Rest of functionality for class A};
¿Como puedes ver? La plantilla singleton supone que su especialización para cualquier tipo Xse heredará singleton<X>y, por lo tanto, tendrá todos sus miembros (públicos, protegidos) accesibles, incluido el GetInstance! Hay otros usos útiles de CRTP. Por ejemplo, si desea contar todas las instancias que existen actualmente para su clase, pero desea encapsular esta lógica en una plantilla separada (la idea de una clase concreta es bastante simple: tenga una variable estática, incremente en ctors, disminuya en dtors ) ¡Intenta hacerlo como ejercicio!
Otro ejemplo útil más para Boost (no estoy seguro de cómo lo han implementado, pero CRTP también lo hará). ¡Imagina que deseas proporcionar solo un operador <para tus clases pero automáticamente un operador ==para ellas!
podrías hacerlo así:
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 <}
Esto podría parecer que usted escribiría menos si se acaba de escribir operador ==para Apple, pero imagina que la Equalityplantilla proporcionaría no sólo ==pero >, >=, <=etc, y se podría utilizar estas definiciones para múltiples clases, la reutilización del código!
Este post no aboga por Singleton como pattern.it buena programando utiliza simplemente como una ilustración que puede ser comúnmente understood.imo la-1 es injustificada
John Dibling
3
@Armen: La respuesta explica CRTP de una manera que se puede entender claramente, es una buena respuesta, gracias por tan buena respuesta.
Alok Save
1
@Armen: gracias por esta gran explicación. ¡Antes estaba recibiendo CRTP, pero el ejemplo de igualdad ha sido esclarecedor! +1
Paul
1
Otro ejemplo más de uso de CRTP es cuando necesita una clase no copiable: plantilla <clase T> clase NonCopyable {protegido: NonCopyable () {} ~ NonCopyable () {} privado: NonCopyable (const NonCopyable &); NonCopyable & operator = (const NonCopyable &); }; Luego usa noncopiable como se muestra a continuación: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren
2
@Cachorro: Singleton no es terrible. Los programadores por debajo del promedio lo utilizan en exceso cuando otros enfoques serían más apropiados, pero el hecho de que la mayoría de sus usos sean terribles no hace que el patrón en sí sea terrible. Hay casos en los que Singleton es la mejor opción, aunque son raros.
Kaiserludi
47
Aquí puedes ver un gran ejemplo. Si usa un método virtual, el programa sabrá qué ejecutar en tiempo de ejecución. Implementando CRTP el compilador es el que decide en tiempo de compilación !!! Esta es una gran actuación!
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);}};
¿No podrías hacer esto definiendo virtual void write(const char* str) const = 0;? Aunque para ser justos, esta técnica parece muy útil cuando se writeestá haciendo otro trabajo.
atlex2
26
Usando un método virtual puro, está resolviendo la herencia en tiempo de ejecución en lugar de tiempo de compilación. CRTP se utiliza para resolver esto en tiempo de compilación para que la ejecución sea más rápida.
GutiMac
1
Intente hacer una función simple que espere un escritor abstracto: no puede hacerlo porque no hay una clase llamada escritor en ningún lado, entonces, ¿dónde está exactamente su polimorfismo? Esto no es equivalente a las funciones virtuales y es mucho menos útil.
22
CRTP es una técnica para implementar el polimorfismo en tiempo de compilación. Aquí hay un ejemplo muy simple. En el siguiente ejemplo, ProcessFoo()está trabajando con Basela interfaz de clase e Base::Fooinvoca el foo()método del objeto derivado , que es lo que pretende hacer con los métodos virtuales.
También podría valer la pena en este ejemplo agregar un ejemplo de cómo implementar un foo () predeterminado en la clase Base que se llamará si ningún Derivado lo ha implementado. AKA cambie foo en la Base a otro nombre (p. Ej., Llamador ()), agregue una nueva función foo () a la Base que cout es "Base". Luego llame a la persona que llama () dentro de ProcessFoo
wizurd
@wizurd Este ejemplo es más para ilustrar una función de clase base virtual pura, es decir, exigimos que foo()sea implementada por la clase derivada.
blueskin
3
Esta es mi respuesta favorita, ya que también muestra por qué este patrón es útil con la ProcessFoo()función.
Pietro
No entiendo el punto de este código, porque con void ProcessFoo(T* b)y sin tener Derived y AnotherDerived realmente derivado, todavía funcionaría. En mi humilde opinión, sería más interesante si ProcessFoo no hiciera uso de plantillas de alguna manera.
Gabriel Devillers
1
@GabrielDevillers En primer lugar, la plantilla ProcessFoo()funcionará con cualquier tipo que implemente la interfaz, es decir, en este caso, el tipo de entrada T debería tener un método llamado foo(). En segundo lugar, con el fin de obtener una no plantilla ProcessFoopara trabajar con múltiples tipos, es probable que termine usando RTTI, que es lo que queremos evitar. Además, la versión con plantilla proporciona una verificación del tiempo de compilación en la interfaz.
piel azul
6
Esta no es una respuesta directa, sino un ejemplo de cómo CRTP puede ser útil.
Un buen ejemplo concreto de CRTP es std::enable_shared_from_thisde C ++ 11:
Una clase Tpuede heredar de enable_shared_from_this<T>para heredar las shared_from_thisfunciones miembro que obtienen una shared_ptrinstancia que apunta *this.
Es decir, heredar de std::enable_shared_from_thishace posible obtener un puntero compartido (o débil) a su instancia sin acceso a él (por ejemplo, desde una función miembro de la que solo conoce *this).
Es útil cuando necesita dar un std::shared_ptrpero solo tiene acceso a *this:
La razón por la que no puede pasar thisdirectamente en lugar de shared_from_this()es porque rompería el mecanismo de propiedad:
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);
lo siento, lo malo, static_cast se encarga del cambio. Si desea ver el caso de la esquina de todos modos, aunque no cause error, vea aquí: ideone.com/LPkktf
odinthenerd
30
Mal ejemplo. Este código podría hacerse sin vtables sin usar CRTP. Lo que vtablerealmente proporciona es usar la clase base (puntero o referencia) para llamar a métodos derivados. Debe mostrar cómo se hace con CRTP aquí.
Etherealone
17
En tu ejemplo, Base<>::method ()ni siquiera se llama, ni usas polimorfismo en ningún lado.
MikeMB
1
@Jichao, según la nota @MikeMB 's, debe llamar methodImplen el methodde Basey en las clases derivadas nombrar methodImplen lugar demethod
Ivan Kush
1
si usa un método similar (), entonces está estáticamente vinculado y no necesita la clase base común. Porque de todos modos no podrías usarlo polimórficamente a través del puntero de clase base o ref. Entonces el código debería verse así: #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"; }};
Respuestas:
En resumen, CRTP es cuando una clase
A
tiene una clase base que es una especialización de plantilla para la claseA
misma. P.ejSe está curiosamente recurrente, no es así? :)
Ahora, ¿qué te da esto? Esto realmente le da a la
X
plantilla la capacidad de ser una clase base para sus especializaciones.Por ejemplo, podría hacer una clase singleton genérica (versión simplificada) como esta
Ahora, para que una clase arbitraria sea
A
un singleton, debes hacer esto¿Como puedes ver? La plantilla singleton supone que su especialización para cualquier tipo
X
se heredarásingleton<X>
y, por lo tanto, tendrá todos sus miembros (públicos, protegidos) accesibles, incluido elGetInstance
! Hay otros usos útiles de CRTP. Por ejemplo, si desea contar todas las instancias que existen actualmente para su clase, pero desea encapsular esta lógica en una plantilla separada (la idea de una clase concreta es bastante simple: tenga una variable estática, incremente en ctors, disminuya en dtors ) ¡Intenta hacerlo como ejercicio!Otro ejemplo útil más para Boost (no estoy seguro de cómo lo han implementado, pero CRTP también lo hará). ¡Imagina que deseas proporcionar solo un operador
<
para tus clases pero automáticamente un operador==
para ellas!podrías hacerlo así:
Ahora puedes usarlo así
Ahora, usted no ha proporcionado explícitamente operador
==
paraApple
? Pero lo tienes! Puedes escribirEsto podría parecer que usted escribiría menos si se acaba de escribir operador
==
paraApple
, pero imagina que laEquality
plantilla proporcionaría no sólo==
pero>
,>=
,<=
etc, y se podría utilizar estas definiciones para múltiples clases, la reutilización del código!CRTP es una cosa maravillosa :) HTH
fuente
Aquí puedes ver un gran ejemplo. Si usa un método virtual, el programa sabrá qué ejecutar en tiempo de ejecución. Implementando CRTP el compilador es el que decide en tiempo de compilación !!! Esta es una gran actuación!
fuente
virtual void write(const char* str) const = 0;
? Aunque para ser justos, esta técnica parece muy útil cuando sewrite
está haciendo otro trabajo.CRTP es una técnica para implementar el polimorfismo en tiempo de compilación. Aquí hay un ejemplo muy simple. En el siguiente ejemplo,
ProcessFoo()
está trabajando conBase
la interfaz de clase eBase::Foo
invoca elfoo()
método del objeto derivado , que es lo que pretende hacer con los métodos virtuales.http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e
Salida:
fuente
foo()
sea implementada por la clase derivada.ProcessFoo()
función.void ProcessFoo(T* b)
y sin tener Derived y AnotherDerived realmente derivado, todavía funcionaría. En mi humilde opinión, sería más interesante si ProcessFoo no hiciera uso de plantillas de alguna manera.ProcessFoo()
funcionará con cualquier tipo que implemente la interfaz, es decir, en este caso, el tipo de entrada T debería tener un método llamadofoo()
. En segundo lugar, con el fin de obtener una no plantillaProcessFoo
para trabajar con múltiples tipos, es probable que termine usando RTTI, que es lo que queremos evitar. Además, la versión con plantilla proporciona una verificación del tiempo de compilación en la interfaz.Esta no es una respuesta directa, sino un ejemplo de cómo CRTP puede ser útil.
Un buen ejemplo concreto de CRTP es
std::enable_shared_from_this
de C ++ 11:Es decir, heredar de
std::enable_shared_from_this
hace posible obtener un puntero compartido (o débil) a su instancia sin acceso a él (por ejemplo, desde una función miembro de la que solo conoce*this
).Es útil cuando necesita dar un
std::shared_ptr
pero solo tiene acceso a*this
:La razón por la que no puede pasar
this
directamente en lugar deshared_from_this()
es porque rompería el mecanismo de propiedad:fuente
Solo como nota:
CRTP podría usarse para implementar polimorfismos estáticos (que les gusta el polimorfismo dinámico pero sin la tabla de puntero de función virtual).
El resultado sería:
fuente
vtable
s sin usar CRTP. Lo quevtable
realmente proporciona es usar la clase base (puntero o referencia) para llamar a métodos derivados. Debe mostrar cómo se hace con CRTP aquí.Base<>::method ()
ni siquiera se llama, ni usas polimorfismo en ningún lado.methodImpl
en elmethod
deBase
y en las clases derivadas nombrarmethodImpl
en lugar demethod