¿Cuál es el patrón de plantilla curiosamente recurrente (CRTP)?


187

Sin referirse a un libro, ¿alguien puede dar una buena explicación CRTPcon un ejemplo de código?


2
Lea las preguntas de CRTP en SO: stackoverflow.com/questions/tagged/crtp . Eso podría darte una idea.
sbi

68
@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 <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;

Ahora, para que una clase arbitraria sea Aun singleton, debes hacer esto

class A: public Singleton<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<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 <
}

Ahora puedes usarlo así

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

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

Ahora, usted no ha proporcionado explícitamente operador ==para Apple? Pero lo tienes! Puedes escribir

int main()
{
    Apple a1;
    Apple a2; 

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

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!

CRTP es una cosa maravillosa :) HTH


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

¿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.

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

Salida:

derived foo
AnotherDerived foo

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

[util.smartptr.enab] / 1

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:

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

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

5

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

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

El resultado sería:

Derived1 method
Derived2 method

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"; }};
barney
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.