Mixins mediante CRTP
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy — lección 77 de 104.
El Curiously Recurring Template Pattern (CRTP) es una técnica en la que una clase hereda de una clase base de plantilla, pasándose a sí misma como argumento de la plantilla. Esto permite el polimorfismo en tiempo de compilación y permite que las clases base accedan a los miembros de la clase derivada sin funciones virtuales.
La estructura básica de CRTP se ve así:
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 es particularmente potente para crear mixins: funcionalidad reutilizable que se puede "mezclar" en las clases. A diferencia de la herencia tradicional, los mixins añaden capacidades sin crear jerarquías 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(); // Salida: Person: AliceLa ventaja clave es que todas las llamadas a métodos se resuelven en tiempo de compilación, eliminando la sobrecarga de las funciones virtuales. Puedes combinar múltiples mixins CRTP para componer funcionalidad, convirtiéndolo en una alternativa flexible al polimorfismo en tiempo de ejecución cuando los tipos se conocen en tiempo de compilación.
Desafío
FácilVamos a construir un sistema de registro que utiliza mixins CRTP para añadir funcionalidad reutilizable a diferentes clases sin la sobrecarga de las funciones virtuales. Crearás dos mixins que pueden ser "mezclados" en cualquier clase: uno para contar instancias y otro para generar representaciones de cadena.
Organizarás tu código en tres archivos:
Mixins.h: Define dos plantillas de mixin CRTP que proporcionan funcionalidad reutilizable.Crea una plantilla de mixin
Countableque rastree cuántas instancias de una clase derivada existen. Debe tener un contador estático que se incremente en el constructor y se decremente en el destructor. Proporciona un método estáticogetCount()que devuelva el recuento actual.Crea una plantilla de mixin
Describableque proporcione un métododescribe(). Este método debe usarstatic_castpara acceder a la clase derivada y llamar a su métodogetDescription(), luego imprimir el resultado seguido de una nueva línea.Recuerda que los mixins CRTP usan
static_cast<Derived*>(this)para acceder a los miembros de la clase derivada en tiempo de compilación.Entities.h: Define dos clases de entidad que heredan de ambos mixins.Crea una clase
Playerque herede tanto deCountable<Player>como deDescribable<Player>. Debe almacenar un nombre y un nivel, y proporcionar un métodogetDescription()que devuelva una cadena con el formato:Player: [name] (Level [level])Crea una clase
Enemyque también herede de ambos mixins. Debe almacenar un tipo y salud, y proporcionar un métodogetDescription()que devuelva:Enemy: [type] with [health] HPNo olvides inicializar el contador estático para cada clase.
main.cpp: Lee cuatro entradas (cada una en una línea separada):- Nombre del jugador (string)
- Nivel del jugador (integer)
- Tipo de enemigo (string)
- Salud del enemigo (integer)
Crea un Player y un Enemy con los valores dados. Luego demuestra los mixins:
- Imprime
Player count: [count]usando el método estáticogetCount() - Imprime
Enemy count: [count] - Llama a
describe()en el jugador - Llama a
describe()en el enemigo - Crea un segundo jugador con el nombre "Guest" y nivel 1
- Imprime
Player count: [count]de nuevo para mostrar el recuento actualizado - Llama a
describe()en el segundo jugador
Por ejemplo, con las entradas Hero, 10, Dragon, y 500:
Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)Este desafío demuestra cómo los mixins CRTP añaden funcionalidad (contar y describir) a clases no relacionadas sin usar funciones virtuales. Tanto Player como Enemy obtienen las mismas capacidades al heredar de las mismas plantillas de mixin, pero cada una mantiene su propio contador de instancias separado porque la plantilla se instancia con tipos diferentes.
Hoja de referencia
El Patrón de Plantilla Curiosamente Recurrente (CRTP) permite el polimorfismo en tiempo de compilación al hacer que una clase herede de una clase base de plantilla, pasándose a sí misma como argumento de la plantilla:
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";
}
};Los Mixins CRTP proporcionan funcionalidad reutilizable sin jerarquías de herencia 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; }
};Ventajas clave:
- Todas las llamadas a métodos se resuelven en tiempo de compilación
- Sin sobrecarga de funciones virtuales
- Se pueden combinar múltiples mixins para componer funcionalidad
- Cada instanciación de plantilla mantiene un estado separado (por ejemplo, contadores estáticos separados por tipo derivado)
Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Entities.h"
using namespace std;
int main() {
// Leer las entradas
string playerName;
int playerLevel;
string enemyType;
int enemyHealth;
cin >> playerName;
cin >> playerLevel;
cin >> enemyType;
cin >> enemyHealth;
// TODO: Crear un Player con el nombre y nivel dados
// TODO: Crear un Enemy con el tipo y salud dados
// TODO: Imprimir "Player count: [count]" usando Player::getCount()
// TODO: Imprimir "Enemy count: [count]" usando Enemy::getCount()
// TODO: Llamar a describe() en el player
// TODO: Llamar a describe() en el enemy
// TODO: Crear un segundo player con nombre "Guest" y nivel 1
// TODO: Imprimir "Player count: [count]" de nuevo
// TODO: Llamar a describe() en el segundo player
return 0;
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico