Menu
Coddy logo textTech

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: Alice

La 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.

challenge icon

Desafío

Fácil

Vamos 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 Countable que 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ático getCount() que devuelva el recuento actual.

    Crea una plantilla de mixin Describable que proporcione un método describe(). Este método debe usar static_cast para acceder a la clase derivada y llamar a su método getDescription(), 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 Player que herede tanto de Countable<Player> como de Describable<Player>. Debe almacenar un nombre y un nivel, y proporcionar un método getDescription() que devuelva una cadena con el formato: Player: [name] (Level [level])

    Crea una clase Enemy que también herede de ambos mixins. Debe almacenar un tipo y salud, y proporcionar un método getDescription() que devuelva: Enemy: [type] with [health] HP

    No olvides inicializar el contador estático para cada clase.

  • main.cpp: Lee cuatro entradas (cada una en una línea separada):
    1. Nombre del jugador (string)
    2. Nivel del jugador (integer)
    3. Tipo de enemigo (string)
    4. Salud del enemigo (integer)

    Crea un Player y un Enemy con los valores dados. Luego demuestra los mixins:

    1. Imprime Player count: [count] usando el método estático getCount()
    2. Imprime Enemy count: [count]
    3. Llama a describe() en el jugador
    4. Llama a describe() en el enemigo
    5. Crea un segundo jugador con el nombre "Guest" y nivel 1
    6. Imprime Player count: [count] de nuevo para mostrar el recuento actualizado
    7. 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;
}
quiz iconPonte a prueba

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