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: AliceA 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.
Desafio
FácilVamos 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
Countableque 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áticogetCount()que retorna a contagem atual.Crie um template de mixin
Describableque fornece um métododescribe(). Este método deve usarstatic_castpara acessar a classe derivada e chamar seu métodogetDescription(), 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
Playerque herda deCountable<Player>eDescribable<Player>. Ela deve armazenar um nome e um nível, e fornecer um métodogetDescription()que retorna uma string no formato:Player: [name] (Level [level])Crie uma classe
Enemyque também herda de ambos os mixins. Ela deve armazenar um tipo e vida (health), e fornecer um métodogetDescription()que retorna:Enemy: [type] with [health] HPNão se esqueça de inicializar o contador estático para cada classe.
main.cpp: Leia quatro entradas (cada uma em uma linha separada):- Nome do jogador (string)
- Nível do jogador (inteiro)
- Tipo do inimigo (string)
- Vida do inimigo (inteiro)
Crie um Player e um Enemy com os valores fornecidos. Em seguida, demonstre os mixins:
- Imprima
Player count: [count]usando o método estáticogetCount() - Imprima
Enemy count: [count] - Chame
describe()no jogador - Chame
describe()no inimigo - Crie um segundo jogador com o nome "Guest" e nível 1
- Imprima
Player count: [count]novamente para mostrar a contagem atualizada - 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;
}
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
1Fundamentos de OOP
Arquivos ExternosBuild e Compilação em C++Arquivos Header e SourceNamespaces e EscopoIntrodução a OOP em C++Classes vs ObjetosO Ponteiro 'this'Métodos (Funções Membro)Atributos (Membros de Dados)Básico de Ctors e DtorsRecapitulação - Calculadora Simples4Propriedades de Classe
Membros de Instância vs EstáticosGetters e SettersFunções de Membro ConstPalavra-chave MutableMétodos e Variáveis EstáticosFunções e Classes FriendRecapitulação - Gerenciador de Conta Bancária7Herança
Herança BásicaNíveis de Acesso na HerançaOrdem de Chamada de Ctor e DtorSobrescrita de MétodosFunções Virtuais e VTableHerança MúltiplaHerança VirtualRecapitulação - Hierarquia de Funcionários2Gerenciamento de Memória
Memória Stack vs HeapPonteiros e ReferênciasMemória Dinâmica (new/delete)Smart Pointers em C++RAII em C++Recapitulação - Gerenciador de Array Dinâmico5Encapsulamento
Especificadores de Acesso em C++Especificadores de Acesso em DetalhesOcultação de InformaçãoStruct vs ClassClasses Aninhadas e InternasRecapitulação - Sistema de Registros de Alunos8Polimorfismo
Polimorfismo de Compilação vs Tempo de ExecuçãoSobrecarga de FunçõesFunções Virtuais RevisitadasFunções Virtuais PurasClasses AbstratasDesign de Interfaces em C++Dynamic Casting e RTTIRecapitulação - Calculadora de Formas11Conceitos Avançados de POO
Composição vs HerançaMixins via CRTPPimpl IdiomType ErasureEnum Classes e Tipagem ForteTratamento de Exceções em POOHierarquias de Exceções Personalizadas3Construtores e Destrutores
Construtor PadrãoConstrutor ParametrizadoConstrutor de CópiaConstrutor de MovimentaçãoListas de Inicialização do ConstrutorConstrutores DelegadosMergulho Profundo em DestrutoresRegra dos Três / Cinco / ZeroRecapitulação - Classe String6Sobrecarga de Operadores
Introdução à Sobrecarga de OperadoresSobrecarga de Operadores AritméticosSobrecarga de Operadores de ComparaçãoOperadores de StreamSobrecarga do Operador de AtribuiçãoSobrecarga dos Operadores [] e ()Operadores de Conversão de TipoRecapitulação - Classe Matrix9Templates
Templates de FunçãoTemplates de ClasseEspecialização de TemplatesTemplates VariádicosFundamentos de SFINAE e Type TraitsRecapitulação - Container Genérico