Padrão de template curiosamente recorrente - Curiously recurring template pattern

O padrão de modelo curiosamente recorrente ( CRTP ) é um idioma em C ++ no qual uma classe Xderiva de uma instanciação de modelo de classe usando a Xsi mesma como um argumento de modelo. Mais geralmente, é conhecida como polimorfismo F-ligado , e é uma forma de F -bounded quantificação .

História

A técnica foi formalizada em 1989 como " quantificação F -bound". O nome "CRTP" foi cunhado independentemente por Jim Coplien em 1995, que o observou em alguns dos primeiros modelos de código C ++ , bem como em exemplos de código que Timothy Budd criou em sua linguagem multiparadigma Leda. Às vezes é chamada de "Herança de cabeça para baixo" devido à maneira como permite que as hierarquias de classes sejam estendidas substituindo diferentes classes de base.

A implementação da Microsoft de CRTP na Active Template Library (ATL) foi descoberta independentemente, também em 1995, por Jan Falkin, que acidentalmente derivou uma classe base de uma classe derivada. Christian Beaumont viu pela primeira vez o código de Jan e inicialmente pensou que ele não poderia ser compilado no compilador da Microsoft disponível na época. Após a revelação de que realmente funcionou, Christian baseou todo o design da ATL e da Biblioteca de Modelos do Windows (WTL) nesse erro.

Forma geral

// The Curiously Recurring Template Pattern (CRTP)
template <class T>
class Base
{
    // methods within Base can use template to access members of Derived
};
class Derived : public Base<Derived>
{
    // ...
};

Alguns casos de uso para esse padrão são polimorfismo estático e outras técnicas de metaprogramação, como as descritas por Andrei Alexandrescu em Modern C ++ Design . Ele também figura com destaque na implementação C ++ do paradigma de Dados, Contexto e Interação .

Polimorfismo estático

Normalmente, o modelo de classe base aproveitará o fato de que os corpos de função de membro (definições) não são instanciados até muito depois de suas declarações e usará membros da classe derivada em suas próprias funções de membro, por meio do uso de um elenco ; por exemplo:

template <class T> 
struct Base
{
    void interface()
    {
        // ...
        static_cast<T*>(this)->implementation();
        // ...
    }

    static void static_func()
    {
        // ...
        T::static_sub_func();
        // ...
    }
};

struct Derived : Base<Derived>
{
    void implementation();
    static void static_sub_func();
};

No exemplo acima, observe em particular que a função Base <Derived> :: deployment (), embora declarada antes da existência da estrutura Derived ser conhecida pelo compilador (ou seja, antes de Derived ser declarada), não é realmente instanciada pelo compilador até que seja realmente chamado por algum código posterior que ocorre após a declaração de Derived (não mostrado no exemplo acima), de modo que no momento em que a função "implementação" é instanciada, a declaração de Derived :: implementação () é conhecida .

Esta técnica atinge um efeito semelhante ao uso de funções virtuais , sem os custos (e alguma flexibilidade) do polimorfismo dinâmico . Esse uso específico do CRTP foi chamado de "ligação dinâmica simulada" por alguns. Esse padrão é amplamente usado nas bibliotecas Windows ATL e WTL .

Para elaborar o exemplo acima, considere uma classe base sem funções virtuais . Sempre que a classe base chama outra função de membro, ela sempre chama suas próprias funções de classe base. Quando derivamos uma classe desta classe base, herdamos todas as variáveis ​​de membro e funções de membro que não foram substituídas (sem construtores ou destruidores). Se a classe derivada chama uma função herdada que, em seguida, chama outra função de membro, essa função nunca chamará nenhuma função de membro derivada ou substituída na classe derivada.

No entanto, se as funções de membro da classe base usarem CRTP para todas as chamadas de função de membro, as funções substituídas na classe derivada serão selecionadas em tempo de compilação. Isso emula efetivamente o sistema de chamada de função virtual em tempo de compilação sem os custos em tamanho ou sobrecarga de chamada de função ( estruturas VTBL e pesquisas de método, maquinário VTBL de herança múltipla) com a desvantagem de não ser capaz de fazer essa escolha em tempo de execução.

Contador de objetos

O objetivo principal de um contador de objetos é recuperar estatísticas de criação e destruição de objetos para uma determinada classe. Isso pode ser facilmente resolvido usando CRTP:

template <typename T>
struct counter
{
    static inline int objects_created = 0;
    static inline int objects_alive = 0;

    counter()
    {
        ++objects_created;
        ++objects_alive;
    }
    
    counter(const counter&)
    {
        ++objects_created;
        ++objects_alive;
    }
protected:
    ~counter() // objects should never be removed through pointers of this type
    {
        --objects_alive;
    }
};

class X : counter<X>
{
    // ...
};

class Y : counter<Y>
{
    // ...
};

Cada vez que um objeto de classe Xé criado, o construtor de counter<X>é chamado, incrementando a contagem criada e viva. Cada vez que um objeto de classe Xé destruído, a contagem de vida é diminuída. É importante observar que counter<X>e counter<Y>são duas classes distintas e é por isso que manterão contagens separadas de Xs e Ys. Neste exemplo de CRTP, essa distinção de classes é o único uso do parâmetro de modelo ( Tin counter<T>) e a razão pela qual não podemos usar uma classe base simples não modelada.

Encadeamento polimórfico

O encadeamento de métodos , também conhecido como idioma de parâmetro nomeado, é uma sintaxe comum para invocar várias chamadas de método em linguagens de programação orientadas a objetos. Cada método retorna um objeto, permitindo que as chamadas sejam encadeadas em uma única instrução, sem a necessidade de variáveis ​​para armazenar os resultados intermediários.

Quando o padrão de objeto de parâmetro nomeado é aplicado a uma hierarquia de objeto, as coisas podem dar errado. Suponha que tenhamos essa classe base:

class Printer
{
public:
    Printer(ostream& pstream) : m_stream(pstream) {}
 
    template <typename T>
    Printer& print(T&& t) { m_stream << t; return *this; }
 
    template <typename T>
    Printer& println(T&& t) { m_stream << t << endl; return *this; }
private:
    ostream& m_stream;
};

As impressões podem ser facilmente encadeadas:

Printer(myStream).println("hello").println(500);

No entanto, se definirmos a seguinte classe derivada:

class CoutPrinter : public Printer
{
public:
    CoutPrinter() : Printer(cout) {}

    CoutPrinter& SetConsoleColor(Color c)
    {
        // ...
        return *this;
    }
};

nós "perdemos" a classe concreta assim que invocamos uma função da base:

//                           v----- we have a 'Printer' here, not a 'CoutPrinter'
CoutPrinter().print("Hello ").SetConsoleColor(Color.red).println("Printer!"); // compile error

Isso acontece porque 'imprimir' é uma função da base - 'Impressora' - e então retorna uma instância de 'Impressora'.

O CRTP pode ser usado para evitar tal problema e implementar o "encadeamento polimórfico":

// Base class
template <typename ConcretePrinter>
class Printer
{
public:
    Printer(ostream& pstream) : m_stream(pstream) {}
 
    template <typename T>
    ConcretePrinter& print(T&& t)
    {
        m_stream << t;
        return static_cast<ConcretePrinter&>(*this);
    }
 
    template <typename T>
    ConcretePrinter& println(T&& t)
    {
        m_stream << t << endl;
        return static_cast<ConcretePrinter&>(*this);
    }
private:
    ostream& m_stream;
};
 
// Derived class
class CoutPrinter : public Printer<CoutPrinter>
{
public:
    CoutPrinter() : Printer(cout) {}
 
    CoutPrinter& SetConsoleColor(Color c)
    {
        // ...
        return *this;
    }
};
 
// usage
CoutPrinter().print("Hello ").SetConsoleColor(Color.red).println("Printer!");

Construção de cópia polimórfica

Ao usar polimorfismo, às vezes é necessário criar cópias de objetos pelo ponteiro da classe base. Um idioma comumente usado para isso é adicionar uma função de clone virtual que é definida em cada classe derivada. O CRTP pode ser usado para evitar a duplicação dessa função ou de outras funções semelhantes em cada classe derivada.

// Base class has a pure virtual function for cloning
class AbstractShape {
public:
    virtual ~AbstractShape () = default;
    virtual std::unique_ptr<AbstractShape> clone() const = 0;
};

// This CRTP class implements clone() for Derived
template <typename Derived>
class Shape : public AbstractShape {
public:
    std::unique_ptr<AbstractShape> clone() const override {
        return std::make_unique<Derived>(static_cast<Derived const&>(*this));
    }

protected:
   // We make clear Shape class needs to be inherited
   Shape() = default;
   Shape(const Shape&) = default;
   Shape(Shape&&) = default;
};

// Every derived class inherits from CRTP class instead of abstract class

class Square : public Shape<Square>{};

class Circle : public Shape<Circle>{};

Isso permite a obtenção de cópias de quadrados, círculos ou quaisquer outras formas por shapePtr->clone().

Armadilhas

Um problema com o polimorfismo estático é que, sem usar uma classe base geral como AbstractShapeno exemplo acima, as classes derivadas não podem ser armazenadas homogeneamente - isto é, colocar diferentes tipos derivados da mesma classe base no mesmo contêiner. Por exemplo, um contêiner definido como std::vector<Shape*>não funciona porque Shapenão é uma classe, mas um modelo que precisa de especialização. Um contêiner definido como std::vector<Shape<Circle>*>só pode armazenar Circles, não Squares. Isso ocorre porque cada uma das classes derivadas da classe base CRTP Shapeé um tipo exclusivo. Uma solução comum para este problema é herdar de uma classe base compartilhada com um destruidor virtual, como o AbstractShapeexemplo acima, permitindo a criação de um std::vector<AbstractShape*>.

Veja também

Referências