Menu
Coddy logo textTech

Mixins via CRTP

Parte da seção Programação Orientada a Objetos do Journey de C++ da Coddy — lição 77 de 104.

O Curiously Recurring Template Pattern (CRTP) é uma técnica onde uma classe herda de uma classe base de template, passando a si mesma como o argumento do template. Isso permite o polimorfismo em tempo de compilação e possibilita que classes base acessem membros de classes derivadas sem o uso de funções virtuais.

A estrutura básica do CRTP se parece com isto:

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class MyClass : public Base<MyClass> {
public:
    void implementation() {
        std::cout << "MyClass implementation\n";
    }
};

O CRTP é particularmente poderoso para criar mixins — funcionalidade reutilizável que pode ser "misturada" em classes. Ao contrário da herança tradicional, os mixins adicionam recursos sem criar hierarquias profundas:

template <typename Derived>
class Printable {
public:
    void print() const {
        const Derived& self = static_cast<const Derived&>(*this);
        std::cout << self.toString() << "\n";
    }
};

class Person : public Printable<Person> {
    std::string name;
public:
    Person(const std::string& n) : name(n) {}
    std::string toString() const { return "Person: " + name; }
};

// Uso:
Person p("Alice");
p.print();  // Saída: Person: Alice

A principal vantagem é que todas as chamadas de método são resolvidas em tempo de compilação, eliminando a sobrecarga de funções virtuais. Você pode combinar múltiplos mixins CRTP para compor funcionalidades, tornando-o uma alternativa flexível ao polimorfismo em tempo de execução quando os tipos são conhecidos em tempo de compilação.

challenge icon

Desafio

Fácil

Vamos construir um sistema de log que utiliza mixins CRTP para adicionar funcionalidade reutilizável a diferentes classes sem a sobrecarga de funções virtuais. Você criará dois mixins que podem ser "misturados" em qualquer classe: um para contar instâncias e outro para gerar representações em string.

Você organizará seu código em três arquivos:

  • Mixins.h: Defina dois templates de mixin CRTP que fornecem funcionalidade reutilizável.

    Crie um template de mixin Countable que rastreia quantas instâncias de uma classe derivada existem. Ele deve ter um contador estático que incrementa no construtor e decrementa no destrutor. Forneça um método estático getCount() que retorna a contagem atual.

    Crie um template de mixin Describable que fornece um método describe(). Este método deve usar static_cast para acessar a classe derivada e chamar seu método getDescription(), imprimindo o resultado seguido por uma nova linha.

    Lembre-se que mixins CRTP usam static_cast<Derived*>(this) para acessar os membros da classe derivada em tempo de compilação.

  • Entities.h: Defina duas classes de entidade que herdam de ambos os mixins.

    Crie uma classe Player que herda de Countable<Player> e Describable<Player>. Ela deve armazenar um nome e um nível, e fornecer um método getDescription() que retorna uma string no formato: Player: [name] (Level [level])

    Crie uma classe Enemy que também herda de ambos os mixins. Ela deve armazenar um tipo e vida (health), e fornecer um método getDescription() que retorna: Enemy: [type] with [health] HP

    Não se esqueça de inicializar o contador estático para cada classe.

  • main.cpp: Leia quatro entradas (cada uma em uma linha separada):
    1. Nome do jogador (string)
    2. Nível do jogador (inteiro)
    3. Tipo do inimigo (string)
    4. Vida do inimigo (inteiro)

    Crie um Player e um Enemy com os valores fornecidos. Em seguida, demonstre os mixins:

    1. Imprima Player count: [count] usando o método estático getCount()
    2. Imprima Enemy count: [count]
    3. Chame describe() no jogador
    4. Chame describe() no inimigo
    5. Crie um segundo jogador com o nome "Guest" e nível 1
    6. Imprima Player count: [count] novamente para mostrar a contagem atualizada
    7. Chame describe() no segundo jogador

Por exemplo, com as entradas Hero, 10, Dragon, e 500:

Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)

Este desafio demonstra como os mixins CRTP adicionam funcionalidade (contagem e descrição) a classes não relacionadas sem usar funções virtuais. Tanto Player quanto Enemy ganham as mesmas capacidades ao herdar dos mesmos templates de mixin, mas cada um mantém seu próprio contador de instâncias separado porque o template é instanciado com tipos diferentes.

Folha de consulta

O Curiously Recurring Template Pattern (CRTP) permite o polimorfismo em tempo de compilação ao fazer com que uma classe herde de uma classe base de template, passando a si mesma como o argumento do template:

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class MyClass : public Base<MyClass> {
public:
    void implementation() {
        std::cout << "MyClass implementation\n";
    }
};

CRTP Mixins fornecem funcionalidade reutilizável sem hierarquias de herança profundas:

template <typename Derived>
class Printable {
public:
    void print() const {
        const Derived& self = static_cast<const Derived&>(*this);
        std::cout << self.toString() << "\n";
    }
};

class Person : public Printable<Person> {
    std::string name;
public:
    Person(const std::string& n) : name(n) {}
    std::string toString() const { return "Person: " + name; }
};

Principais vantagens:

  • Todas as chamadas de método são resolvidas em tempo de compilação
  • Sem sobrecarga (overhead) de funções virtuais
  • Múltiplos mixins podem ser combinados para compor funcionalidades
  • Cada instanciação de template mantém um estado separado (ex: contadores estáticos separados por tipo derivado)

Experimente você mesmo

#include <iostream>
#include <string>
#include "Entities.h"

using namespace std;

int main() {
    // Ler as entradas
    string playerName;
    int playerLevel;
    string enemyType;
    int enemyHealth;
    
    cin >> playerName;
    cin >> playerLevel;
    cin >> enemyType;
    cin >> enemyHealth;
    
    // TODO: Criar um Player com o nome e nível fornecidos
    
    // TODO: Criar um Enemy com o tipo e vida fornecidos
    
    // TODO: Imprimir "Player count: [count]" usando Player::getCount()
    
    // TODO: Imprimir "Enemy count: [count]" usando Enemy::getCount()
    
    // TODO: Chamar describe() no player
    
    // TODO: Chamar describe() no enemy
    
    // TODO: Criar um segundo player com o nome "Guest" e nível 1
    
    // TODO: Imprimir "Player count: [count]" novamente
    
    // TODO: Chamar describe() no segundo player
    
    return 0;
}
quiz iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Programação Orientada a Objetos