C++
Patrón de Plantilla Curiosamente Recurrente (CRTP)
Buscar..
Introducción
Un patrón en el que una clase hereda de una plantilla de clase consigo misma como uno de sus parámetros de plantilla. CRTP se usa generalmente para proporcionar polimorfismo estático en C ++.
El patrón de plantilla curiosamente recurrente (CRTP)
CRTP es una poderosa alternativa estática a las funciones virtuales y la herencia tradicional que se puede usar para dar propiedades de tipos en tiempo de compilación. Funciona al tener una plantilla de clase base que toma, como uno de sus parámetros de plantilla, la clase derivada. Esto permite que se realice legalmente una static_cast
de su this
puntero a la clase derivada.
Por supuesto, esto también significa que una clase CRTP siempre debe usarse como la clase base de alguna otra clase. Y la clase derivada debe pasar a la clase base.
Supongamos que tiene un conjunto de contenedores que admiten las funciones begin()
y end()
. Los requisitos de la biblioteca estándar para contenedores requieren más funcionalidad. Podemos diseñar una clase base CRTP que proporcione esa funcionalidad, basada únicamente en begin()
y end()
:
#include <iterator>
template <typename Sub>
class Container {
private:
// self() yields a reference to the derived type
Sub& self() { return *static_cast<Sub*>(this); }
Sub const& self() const { return *static_cast<Sub const*>(this); }
public:
decltype(auto) front() {
return *self().begin();
}
decltype(auto) back() {
return *std::prev(self().end());
}
decltype(auto) size() const {
return std::distance(self().begin(), self().end());
}
decltype(auto) operator[](std::size_t i) {
return *std::next(self().begin(), i);
}
};
La clase anterior proporciona las funciones front()
, back()
, size()
y operator[]
para cualquier subclase que proporcione begin()
y end()
. Un ejemplo de subclase es una matriz simple asignada dinámicamente:
#include <memory>
// A dynamically allocated array
template <typename T>
class DynArray : public Container<DynArray<T>> {
public:
using Base = Container<DynArray<T>>;
DynArray(std::size_t size)
: size_{size},
data_{std::make_unique<T[]>(size_)}
{ }
T* begin() { return data_.get(); }
const T* begin() const { return data_.get(); }
T* end() { return data_.get() + size_; }
const T* end() const { return data_.get() + size_; }
private:
std::size_t size_;
std::unique_ptr<T[]> data_;
};
Los usuarios de la clase DynArray
pueden usar las interfaces proporcionadas por la clase base CRTP de la siguiente manera:
DynArray<int> arr(10);
arr.front() = 2;
arr[2] = 5;
assert(arr.size() == 10);
Utilidad: este patrón evita particularmente las llamadas a funciones virtuales en tiempo de ejecución que ocurren para atravesar la jerarquía de herencia y simplemente se basan en conversiones estáticas:
DynArray<int> arr(10);
DynArray<int>::Base & base = arr;
base.begin(); // no virtual calls
La única Container<DynArray<int>>
estática dentro de la función begin()
en el Container<DynArray<int>>
clase base Container<DynArray<int>>
permite que el compilador optimice drásticamente el código y no se realicen búsquedas de tablas virtuales en el tiempo de ejecución.
Limitaciones: debido a que la clase base tiene una plantilla y es diferente para dos DynArray
s diferentes, no es posible almacenar los punteros a sus clases base en una matriz de tipo homogéneo como se podría hacer con la herencia normal, donde la clase base no depende de la derivada tipo:
class A {};
class B: public A{};
A* a = new B;
CRTP para evitar la duplicación de código
El ejemplo en Visitor Pattern proporciona un caso de uso convincente para CRTP:
struct IShape
{
virtual ~IShape() = default;
virtual void accept(IShapeVisitor&) const = 0;
};
struct Circle : IShape
{
// ...
// Each shape has to implement this method the same way
void accept(IShapeVisitor& visitor) const override { visitor.visit(*this); }
// ...
};
struct Square : IShape
{
// ...
// Each shape has to implement this method the same way
void accept(IShapeVisitor& visitor) const override { visitor.visit(*this); }
// ...
};
Cada tipo secundario de IShape
necesita implementar la misma función de la misma manera. Eso es un montón de mecanografía extra. En su lugar, podemos introducir un nuevo tipo en la jerarquía que hace esto por nosotros:
template <class Derived>
struct IShapeAcceptor : IShape {
void accept(IShapeVisitor& visitor) const override {
// visit with our exact type
visitor.visit(*static_cast<Derived const*>(this));
}
};
Y ahora, cada forma simplemente necesita heredar del aceptador:
struct Circle : IShapeAcceptor<Circle>
{
Circle(const Point& center, double radius) : center(center), radius(radius) {}
Point center;
double radius;
};
struct Square : IShapeAcceptor<Square>
{
Square(const Point& topLeft, double sideLength) : topLeft(topLeft), sideLength(sideLength) {}
Point topLeft;
double sideLength;
};
No es necesario un código duplicado.