C++
Modello di modello curiosamente ricorrente (CRTP)
Ricerca…
introduzione
Un modello in cui una classe eredita da un modello di classe con se stesso come uno dei suoi parametri del modello. Il CRTP viene solitamente utilizzato per fornire il polimorfismo statico in C ++.
Il modello di modello Curiosamente ricorrente (CRTP)
CRTP è un'alternativa potente e statica alle funzioni virtuali e all'ereditarietà tradizionale che può essere utilizzata per fornire proprietà dei tipi in fase di compilazione. Funziona avendo un modello di classe base che prende, come uno dei suoi parametri template, la classe derivata. Ciò le permette di svolgere legalmente uno static_cast
della sua this
puntatore alla classe derivata.
Ovviamente, ciò significa anche che una classe CRTP deve sempre essere utilizzata come classe base di un'altra classe. E la classe derivata deve passare alla classe base.
Supponiamo che tu abbia un set di contenitori che supportano tutte le funzioni begin()
e end()
. I requisiti della libreria standard per i contenitori richiedono più funzionalità. Possiamo progettare una classe base CRTP che fornisce tale funzionalità, basata esclusivamente su begin()
e 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 classe precedente fornisce le funzioni front()
, back()
, size()
e operator[]
per qualsiasi sottoclasse che fornisce begin()
e end()
. Un esempio di sottoclasse è un semplice array allocato dinamicamente:
#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_;
};
Gli utenti della classe DynArray
possono utilizzare facilmente le interfacce fornite dalla classe base CRTP come segue:
DynArray<int> arr(10);
arr.front() = 2;
arr[2] = 5;
assert(arr.size() == 10);
Utilità: questo modello evita in particolare le chiamate alle funzioni virtuali in fase di esecuzione che si verificano attraversano la gerarchia dell'ereditarietà e si basano semplicemente su valori statici:
DynArray<int> arr(10);
DynArray<int>::Base & base = arr;
base.begin(); // no virtual calls
L'unico cast statico all'interno della funzione begin()
nella classe base Container<DynArray<int>>
consente al compilatore di ottimizzare drasticamente il codice e nessuna ricerca di tabelle virtuali avviene al momento dell'esecuzione.
Limitazioni: Poiché la classe base è basata su modelli e diversa per due diversi DynArray
, non è possibile archiviare i puntatori alle loro classi base in un array omogeneo di tipo come generalmente si potrebbe fare con l'ereditarietà normale in cui la classe base non dipende dal derivato genere:
class A {};
class B: public A{};
A* a = new B;
CRTP per evitare la duplicazione del codice
L'esempio in Visitor Pattern fornisce un caso d'uso convincente per 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); }
// ...
};
Ogni tipo di bambino di IShape
deve implementare la stessa funzione allo stesso modo. Questo è un sacco di digitazione extra. Invece, possiamo introdurre un nuovo tipo nella gerarchia che fa questo per noi:
template <class Derived>
struct IShapeAcceptor : IShape {
void accept(IShapeVisitor& visitor) const override {
// visit with our exact type
visitor.visit(*static_cast<Derived const*>(this));
}
};
E ora, ogni forma deve semplicemente ereditare dall'accettore:
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;
};
Nessun codice duplicato necessario.