Qual é o padrão de modelo curiosamente recorrente (CRTP)?


187

Sem se referir a um livro, alguém pode fornecer uma boa explicação para CRTPum exemplo de código?


2
Leia as perguntas do CRTP no SO: stackoverflow.com/questions/tagged/crtp . Isso pode lhe dar uma idéia.
SBI

68
@sbi: Se ele fizer isso, ele encontrará sua própria pergunta. E isso seria curiosamente recorrente. :)
Craig McQueen

1
BTW, parece-me que o termo deve ser "curiosamente recorrente". Estou entendendo mal o significado?
Craig McQueen

1
Craig: Eu acho que você é; é "curiosamente recorrente" no sentido de que foi encontrado em vários contextos.
precisa saber é o seguinte

Respostas:


276

Em resumo, o CRTP é quando uma classe Atem uma classe base, que é uma especialização de modelo para a Aprópria classe . Por exemplo

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

Ele é curiosamente recorrente, não é? :)

Agora, o que isso lhe dá? Isso realmente dá ao Xmodelo a capacidade de ser uma classe base para suas especializações.

Por exemplo, você pode criar uma classe singleton genérica (versão 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;

Agora, para tornar uma classe arbitrária Aum singleton, você deve fazer isso

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

Então você vê? O modelo singleton assume que sua especialização para qualquer tipo Xserá herdada singleton<X>e, portanto, terá todos os seus membros (públicos, protegidos) acessíveis, incluindo o GetInstance! Existem outros usos úteis do CRTP. Por exemplo, se você deseja contar todas as instâncias que existem atualmente para sua classe, mas deseja encapsular essa lógica em um modelo separado (a idéia para uma classe concreta é bastante simples - tenha uma variável estática, incremento em ctors, decremento em dtors ) Tente fazer isso como um exercício!

Outro exemplo útil, para o Boost (não sei como eles o implementaram, mas o CRTP também o fará). Imagine que você deseja fornecer apenas um operador <para suas classes, mas automaticamente um operador ==para elas!

você poderia fazer assim:

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

Agora você pode usá-lo assim

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

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

Agora, você não forneceu explicitamente operador ==para Apple? Mas você tem! Você pode escrever

int main()
{
    Apple a1;
    Apple a2; 

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

Pode parecer que você escreveria menos se acabasse de escrever ==para o operador Apple, mas imagine que o Equalitymodelo forneceria não apenas , ==mas , etc. E você poderia usar essas definições para várias classes, reutilizando o código!>>=<=

CRTP é uma coisa maravilhosa :) HTH


62
Este post não defende singleton como um pattern.it boa programar simplesmente usa-lo como uma ilustração que pode ser comumente understood.imo the-1 é injustificada
John Dibling

3
@ Armmen: A resposta explica o CRTP de uma maneira que possa ser entendida claramente, é uma boa resposta, obrigado por uma resposta tão boa.
Alok Salvar

1
@ Armmen: obrigado por esta ótima explicação. Eu estava meio que recebendo CRTP antes, mas o exemplo da igualdade tem sido esclarecedor! +1
Paulo

1
Outro exemplo de uso de CRTP é quando você precisa de uma classe não copiável: modelo <classe T> classe NonCopyable {protected: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); NonCopyable & operator = (const Não copiável &); }; Então você usa noncopyable como abaixo: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Virén

2
@ Filhote: Singleton não é terrível. É muito usado em excesso por programadores abaixo da média quando outras abordagens seriam mais apropriadas, mas o fato de a maioria de seus usos ser terrível não torna o padrão em si terrível. Há casos em que o singleton é a melhor opção, embora esses sejam raros.
Kaiserludi

47

Aqui você pode ver um ótimo exemplo. Se você usar o método virtual, o programa saberá o que é executado em tempo de execução. Implementando o CRTP, o compilador é o que decide em tempo de compilação !!! Este é um ótimo desempenho!

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

Você não poderia fazer isso definindo virtual void write(const char* str) const = 0;? Embora seja justo, essa técnica parece super útil quando se writeestá fazendo outro trabalho.
Atlet2

26
Usando um método virtual puro, você está resolvendo a herança em tempo de execução, em vez de tempo de compilação. O CRTP é usado para resolver isso em tempo de compilação, para que a execução seja mais rápida.
GutiMac

1
Tente criar uma função simples que espere um Writer abstrato: você não pode fazê-lo porque não há classe chamada Writer em lugar algum; então, onde está exatamente o seu polimorfismo? Isso não é equivalente a funções virtuais e é muito menos útil.

22

O CRTP é uma técnica para implementar o polimorfismo em tempo de compilação. Aqui está um exemplo muito simples. No exemplo abaixo, ProcessFoo()está trabalhando com a Baseinterface de classe e Base::Foochama o foo()método do objeto derivado , que é o que você pretende fazer com os métodos virtuais.

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

Resultado:

derived foo
AnotherDerived foo

1
Também pode valer a pena neste exemplo adicionar um exemplo de como implementar um foo () padrão na classe Base que será chamado se nenhum Derivado o implementou. AKA mude foo na Base para outro nome (por exemplo, chamador ()), adicione uma nova função foo () à Base que cout a "Base". Então chamador chamada () dentro de ProcessFoo
wizurd

@wizurd Este exemplo é mais para ilustrar uma função pura da classe base virtual, ou seja, aplicamos o que foo()é implementado pela classe derivada.
Blueskin

3
Esta é a minha resposta favorita, pois também mostra por que esse padrão é útil com a ProcessFoo()função.
Pietro

Eu não entendi o ponto desse código, porque com void ProcessFoo(T* b)e sem a derivação de Derived e AnotherDerived, ele ainda funcionaria. IMHO seria mais interessante se o ProcessFoo não fizesse uso de modelos de alguma forma.
Gabriel Devillers 04/06

1
@GabrielDevillers Em primeiro lugar, o modelo ProcessFoo()funcionará com qualquer tipo que implemente a interface, ou seja, nesse caso, o tipo de entrada T deve ter um método chamado foo(). Segundo, para que um modelo não-modelo ProcessFoofuncione com vários tipos, você provavelmente acabará usando o RTTI, que é o que queremos evitar. Além disso, a versão padronizada fornece a verificação do tempo de compilação na interface.
blueskin 12/06

6

Esta não é uma resposta direta, mas um exemplo de como o CRTP pode ser útil.


Um bom exemplo concreto de CRTP é std::enable_shared_from_thisdo C ++ 11:

[util.smartptr.enab] / 1

Uma classe Tpode herdar de enable_­shared_­from_­this<T>para herdar as shared_­from_­thisfunções de membro que obtêm uma shared_­ptrinstância apontando *this.

Ou seja, herdar de std::enable_shared_from_thistorna possível obter um ponteiro compartilhado (ou fraco) para sua instância sem acesso a ela (por exemplo, de uma função membro na qual você conhece apenas *this).

É útil quando você precisa dar um, std::shared_ptrmas você só tem acesso 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));
    }
};

O motivo pelo qual você não pode simplesmente passar thisdiretamente em vez de shared_from_this()é que isso quebraria o mecanismo de propriedade:

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

Apenas como nota:

O CRTP pode ser usado para implementar polimorfismo estático (que gosta de polimorfismo dinâmico, mas sem tabela de ponteiros de função 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;
}

A saída seria:

Derived1 method
Derived2 method

1
desculpe meu mau, static_cast cuida da mudança. Se você quiser ver o caso canto de qualquer maneira, mesmo que ele não causa erro ver aqui: ideone.com/LPkktf
odinthenerd

30
Mau exemplo. Este código pode ser feito sem no vtablesem o uso de CRTP. o quevtable realmente é fornecido é usar a classe base (ponteiro ou referência) para chamar métodos derivados. Você deve mostrar como isso é feito com o CRTP aqui.
Etherealone

17
No seu exemplo, Base<>::method () nem sequer é chamado, nem você usa polimorfismo em lugar algum.
MikeMB

1
@Jichao, de acordo com a nota de @MikeMB, você deve chamar methodImplo nome methodde Basee nas classes derivadasmethodImpl vez demethod
Ivan Kush

1
se você usar o método semelhante (), ele será vinculado estaticamente e você não precisará da classe base comum. De qualquer maneira, você não pode usá-lo polimorficamente através do ponteiro da classe base ou ref. Portanto, o código deve ficar assim: #include <iostream> modelo <nome do tipo T> struct Writer {void write () {static_cast <T *> (this) -> writeImpl (); }}; struct Derivado1: gravador público <Derivado1> {void writeImpl () {std :: cout << "D1"; }}; struct Derived2: public Writer <Derived2> {void writeImpl () {std :: cout << "DER2"; }};
barney
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.