Ir al contenido

Patrones de Metaprogramacion de Templates

Domina patrones avanzados de templates: CRTP para polimorfismo estatico, diseno basado en politicas para composicion flexible, type erasure para genericos en tiempo de ejecucion, y guias sobre cuando usar templates vs despacho virtual.

Mas Alla de los Templates Basicos

Los templates habilitan patrones de diseno que no tienen equivalente en otros lenguajes. Estos patrones explotan la naturaleza en tiempo de compilacion de los templates para lograr polimorfismo estatico (sin overhead de funciones virtuales), comportamiento componible (politicas), y type erasure seguro (ocultar tipos concretos detras de una interfaz uniforme).

Estos no son patrones para principiantes — son herramientas para autores de bibliotecas y disenadores de frameworks. Comprenderlos es esencial para leer los internos de la biblioteca estandar, Boost, y frameworks modernos de C++.

CRTP: Curiously Recurring Template Pattern

En CRTP, una clase deriva de un template instanciado con ella misma como argumento: class Derived : public Base. La clase base puede llamar a metodos de la clase derivada en tiempo de compilacion — sin overhead de despacho virtual. Esto se usa para inyectar comportamiento, implementar polimorfismo estatico, y crear funcionalidad mixin.

crtp.cpp
#include <iostream>

// CRTP base: provides a counter for each derived class
template <typename Derived>
class Counter {
    static inline int count_ = 0;

protected:
    Counter()  { ++count_; }
    ~Counter() { --count_; }
    Counter(const Counter&) { ++count_; }

public:
    static int alive() { return count_; }
};

// Each derived class gets its OWN counter
class Dog : public Counter<Dog> {
public:
    std::string name;
    Dog(std::string n) : name(std::move(n)) {}
};

class Cat : public Counter<Cat> {
public:
    std::string name;
    Cat(std::string n) : name(std::move(n)) {}
};

// CRTP for static polymorphism: no virtual dispatch
template <typename Derived>
class Shape {
public:
    double area() const {
        // Call derived class method — resolved at compile time
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public Shape<Circle> {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area_impl() const { return 3.14159265 * radius_ * radius_; }
};

class Square : public Shape<Square> {
    double side_;
public:
    Square(double s) : side_(s) {}
    double area_impl() const { return side_ * side_; }
};

int main() {
    Dog d1("Rex"), d2("Buddy");
    Cat c1("Whiskers");
    std::cout << "Dogs alive: " << Dog::alive() << '\n'; // 2
    std::cout << "Cats alive: " << Cat::alive() << '\n'; // 1

    Circle circle(5.0);
    Square square(4.0);
    std::cout << "Circle area: " << circle.area() << '\n'; // 78.54
    std::cout << "Square area: " << square.area() << '\n'; // 16.0
}

Diseno Basado en Politicas

El diseno basado en politicas descompone una clase en componentes de comportamiento intercambiables (politicas) pasados como parametros de template. Cada politica implementa un aspecto del comportamiento. Los usuarios componen la combinacion exacta que necesitan. Esto fue popularizado por el libro "Modern C++ Design" de Andrei Alexandrescu.

policy_design.cpp
#include <iostream>
#include <mutex>
#include <stdexcept>

// Threading policies
struct SingleThreaded {
    struct Lock { Lock(SingleThreaded&) {} }; // no-op
};

struct MultiThreaded {
    std::mutex mtx;
    struct Lock {
        std::lock_guard<std::mutex> guard;
        Lock(MultiThreaded& host) : guard(host.mtx) {}
    };
};

// Bounds checking policies
struct NoBoundsCheck {
    static void check(int /*index*/, int /*size*/) {} // no-op
};

struct StrictBoundsCheck {
    static void check(int index, int size) {
        if (index < 0 || index >= size)
            throw std::out_of_range("Index out of bounds");
    }
};

// Policy-based container: users choose their combination
template <typename T,
         typename ThreadingPolicy = SingleThreaded,
         typename BoundsPolicy = NoBoundsCheck>
class SmartArray : private ThreadingPolicy {
    T* data_;
    int size_;

public:
    SmartArray(int size) : data_(new T[size]{}), size_(size) {}
    ~SmartArray() { delete[] data_; }

    T& operator[](int index) {
        typename ThreadingPolicy::Lock lock(*this);
        BoundsPolicy::check(index, size_);
        return data_[index];
    }

    int size() const { return size_; }
};

int main() {
    // Fast, no overhead: no locking, no bounds checks
    SmartArray<int> fast(10);
    fast[0] = 42;

    // Safe: bounds-checked, single-threaded
    SmartArray<int, SingleThreaded, StrictBoundsCheck> safe(10);
    safe[0] = 42;
    try {
        safe[99] = 0; // throws!
    } catch (const std::out_of_range& e) {
        std::cout << e.what() << '\n';
    }

    // Thread-safe with bounds checking
    SmartArray<int, MultiThreaded, StrictBoundsCheck> robust(10);
    robust[5] = 100;
}

Type Erasure

Type erasure oculta el tipo concreto detras de una interfaz uniforme, combinando la flexibilidad de los templates con el polimorfismo en tiempo de ejecucion de las funciones virtuales. El ejemplo clasico es std::function, que puede contener cualquier invocable. El patron usa tres componentes: un concept (interfaz abstracta), un model (wrapper template que implementa la interfaz para cualquier tipo), y una clase handle externa.

type_erasure.cpp
#include <iostream>
#include <memory>
#include <string>

// Type-erased "Printable" — can hold any type with a print() method
class Printable {
    // Concept: abstract interface
    struct Concept {
        virtual ~Concept() = default;
        virtual void print(std::ostream& os) const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };

    // Model: wraps any T that supports operator<<
    template <typename T>
    struct Model : Concept {
        T value;
        Model(T v) : value(std::move(v)) {}
        void print(std::ostream& os) const override { os << value; }
        std::unique_ptr<Concept> clone() const override {
            return std::make_unique<Model>(value);
        }
    };

    std::unique_ptr<Concept> ptr_;

public:
    // Constructor: accepts any printable type
    template <typename T>
    Printable(T value) : ptr_(std::make_unique<Model<T>>(std::move(value))) {}

    // Copy support
    Printable(const Printable& other) : ptr_(other.ptr_->clone()) {}
    Printable& operator=(const Printable& other) {
        ptr_ = other.ptr_->clone();
        return *this;
    }
    Printable(Printable&&) = default;
    Printable& operator=(Printable&&) = default;

    friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
        p.ptr_->print(os);
        return os;
    }
};

int main() {
    // Heterogeneous collection — no common base class needed!
    std::vector<Printable> items;
    items.emplace_back(42);
    items.emplace_back(3.14);
    items.emplace_back(std::string("hello"));
    items.emplace_back('X');

    for (const auto& item : items) {
        std::cout << item << '\n';
    }
    // Output: 42, 3.14, hello, X
}

Expression Templates (Vision General)

Los expression templates son una tecnica donde las expresiones aritmeticas sobre objetos construyen un arbol de operaciones en tiempo de compilacion en lugar de computar resultados intermedios. El resultado final se computa en una sola pasada, eliminando objetos temporales.

Por ejemplo, con una clase Vector ingenua, a + b + c crea dos vectores temporales. Con expression templates, a + b + c construye un objeto de expresion ligero Add, Vector> que computa elementos bajo demanda — sin temporales necesarios.

Esta tecnica es usada por bibliotecas de algebra lineal de alto rendimiento como Eigen y Blaze. El arbol de expresion se evalua de forma diferida (lazily), y el compilador a menudo puede vectorizar el bucle resultante de una sola pasada.

Los expression templates son complejos de implementar correctamente y son principalmente una preocupacion de autores de bibliotecas. Si necesitas matematicas de matrices/vectores, usa una biblioteca como Eigen en lugar de implementar expression templates tu mismo.

Buena práctica

Al elegir entre templates (polimorfismo estatico) y funciones virtuales (polimorfismo dinamico):

Usa templates cuando:
- El conjunto de tipos es conocido en tiempo de compilacion
- Necesitas abstracciones de costo cero (sin vtable, sin indireccion)
- Quieres que el compilador haga inline y optimice a traves de los limites de tipo
- Necesitas trabajar con semantica de valor (sin asignacion en el heap)
- El rendimiento es critico (bucles cerrados, codigo numerico)

Usa funciones virtuales cuando:
- El conjunto de tipos es abierto y se determina en tiempo de ejecucion (plugins, entrada del usuario)
- Necesitas contenedores heterogeneos (por ejemplo, vector>)
- El tamano del binario importa mas que la velocidad (los templates generan mas codigo)
- Quieres ABI estable a traves de los limites de bibliotecas compartidas
- Necesitas agregar nuevos tipos sin recompilar el codigo existente

Usa type erasure cuando:
- Quieres la simplicidad de interfaz del despacho virtual pero no quieres forzar a los usuarios a heredar de tu clase base
- Necesitas semantica de valor con polimorfismo en tiempo de ejecucion (std::function, std::any)

Error común

La metaprogramacion de templates es poderosa pero tiene costos reales:

1. Explosion en tiempo de compilacion — las instanciaciones de templates profundamente anidadas pueden hacer inaceptables los tiempos de compilacion. La TMP recursiva es especialmente mala — prefiere fold expressions e if constexpr.
2. Mensajes de error — sin concepts, los errores de templates producen paginas de texto incomprensible. Siempre restringe tus templates.
3. Depuracion — recorrer codigo pesado en templates con un depurador es doloroso. Los wrappers con type erasure agregan indireccion que oculta el tipo real.
4. Legibilidad — CRTP, diseno basado en politicas y expression templates son dificiles de entender para desarrolladores junior. Documenta el nombre y la intencion del patron.
5. Sobreingenieria — no todos los problemas necesitan una solucion con templates. Una funcion virtual simple o incluso una sentencia switch puede ser mas clara y suficiente.

Puntos clave
  • CRTP habilita polimorfismo estatico y contadores/mixins por clase derivando de Base
  • El diseno basado en politicas descompone el comportamiento en parametros de template — los usuarios componen la combinacion exacta que necesitan
  • Type erasure combina la flexibilidad de templates con polimorfismo en tiempo de ejecucion (por ejemplo, std::function, std::any)
  • Los expression templates eliminan temporales en operaciones encadenadas — usados por Eigen, Blaze y bibliotecas similares
  • Elige templates para tipos conocidos y costo cero; despacho virtual para conjuntos de tipos abiertos y flexibilidad en tiempo de ejecucion
  • Evita la sobreingenieria — usa el mecanismo mas simple que cumpla tus requisitos

Quiz — Pon a prueba tus conocimientos

(20 XP)

1. En CRTP, que logra `class Dog : public Counter<Dog>`?

2. Que problema resuelve type erasure?

3. Cuando deberias preferir funciones virtuales sobre templates?