El Curiously Recurring Template Pattern (CRTP) es una técnica de metaprogramación en la que una clase derivada hereda de una clase base que es una plantilla parametrizada por el tipo de la propia clase derivada, es decir: class Derived : public Base<Derived>. A través de este mecanismo, la clase base puede realizar un static_cast a la clase derivada para acceder a sus miembros, permitiendo que la base “conozca” la estructura de la hija sin necesidad de recurrir a funciones virtuales.
Esto funciona porque la información del tipo de la clase derivada está disponible para la clase base en el momento de la instanciación de la plantilla. Al no depender de una vtable (tabla de funciones virtuales), el compilador tiene la visibilidad completa necesaria para realizar inlining de las llamadas, transformando lo que sería una indirección en tiempo de ejecución en una llamada directa o incluso eliminándola por completo. Este patrón es ideal cuando necesitas implementar mixins de comportamiento (como añadir operator!= basándose en un operator== ya definido) o cuando estás diseñando interfaces de alto rendimiento donde el overhead de un puntero a función virtual es inaceptable. Sin embargo, si intentas tratar a Base<A> y Base<B> como el mismo tipo en una colección heterogénea (como un std::vector<Base*>), te encontrarás con un error de compilación, ya que para el sistema de tipos, cada instanciación de la plantilla es un tipo completamente distinto y no relacionado entre sí.
#include <iostream>
#include <concepts>
#include <vector>
// Interfaz estática mediante CRTP para proporcionar comparaciones
template <typename Derived>
class Comparable {
public:
// Implementamos operator== en la base usando la lógica de la derivada.
// Usamos un concepto [C++20] para asegurar que la derivada tenga el método necesario.
template <typename T>
requires requires(const Derived& d, const T& other) { d.impl_equals(other); }
bool operator==(const T& other) const {
// El static_cast es seguro porque garantizamos la jerarquía mediante el template
return static_cast<const Derived&>(*this).impl_equals(other);
}
// Mixin: Proporcionamos operator!= de forma automática si existe operator==
template <typename T>
bool operator!=(const T& other) const {
return !(*this == other);
}
};
// Clase concreta que implementa la lógica real
class Point : public Comparable<Point> {
public:
int x, y;
constexpr Point(int x, int y) : x(x), y(y) {}
// El método de implementación que la base llamará mediante CRTP
bool impl_equals(const Point& other) const {
return x == other.x && y == other.y;
}
};
int main() {
const Point p1{10, 20};
const Point p2{10, 20};
const Point p3{5, 5};
// Las llamadas a operator== son resoluciones estáticas (en línea)
if (p1 == p2) {
std::cout << "p1 y p2 son iguales\n";
}
if (p1 != p3) {
std::cout << "p1 y p3 son distintos\n";
}
return 0;
}
Análisis del código
En el ejemplo, Comparable<Derived> actúa como un mixin que inyecta capacidades de comparación en cualquier clase que herede de él. Fíjate en static_cast<const Derived&>(*this) dentro de operator==: esta es la clave del CRTP. En lugar de una llamada virtual que buscaría en la vtable en tiempo de ejecución, el compilador sabe exactamente que *this es un Point y genera una llamada directa a impl_equals.
He utilizado una constraint de concepto requires [C++20] para validar que Derived implemente impl_equals. Esto mejora drásticamente la calidad de los mensajes de error en comparación con el C++ tradicional, donde el error estallaría mucho más tarde, durante la instanciación de la función de la clase base.
Es importante notar que operator!= no se define en Point, sino que se “hereda” de Comparable<Point>. Esto es una ventaja de diseño: si mañana implementas operator== en cien clases distintas, solo tienes que escribir la lógica de igualdad; la desigualdad se genera automáticamente para todas ellas sin repetición de código.
Un detalle avanzado: aunque CRTP es extremadamente eficiente, cada vez que instancies Comparable<T>, el compilador generará una nueva clase en el binario. En sistemas con jerarquías masivas, esto puede aumentar el code bloat (crecimiento del tamaño del binario). Una alternativa moderna introducida en C++23 es el uso de deducing this (parámetros de objeto explícitos), que permite implementar patrones similares sin necesidad de templates en la base, aunque el CRTP sigue siendo la herramienta estándar para la reutilización de código mediante mixins.
El error frecuente
Un error conceptual común es intentar usar CRTP para lograr polimorfismo dinámico real. Si intentas crear una colección de objetos derivados que compartan la misma base, el compilador fallará:
// ERROR: Tipos incompatibles std::vector<Comparable<Point>> vec; // ERROR: No hay conversión implícita
Esto ocurre porque Comparable<Point> y Comparable<OtroTipo> son tipos totalmente disjuntos. Si necesitas una colección que contenga diferentes tipos que comparten una interfaz, debes usar una clase base no-template con funciones virtual, o bien, si el rendimiento es crítico y los tipos se conocen en compilación, puedes usar std::variant [C++17].
Si necesitas inspeccionar el tipo exacto en tiempo de ejecución pero quieres mantener la eficiencia de CRTP, el patrón suele combinar una interfaz base con virtual y la implementación CRTP en las capas inferiores, pero eso ya es una arquitectura de híbrida.
La elección entre polimorfismo estático y dinámico debe basarse en si la jerarquía es conocida en tiempo de compilación o si la varianza de tipos es una necesidad en el runtime.
N° 116