C++
Neugierig wiederkehrendes Vorlagenmuster (CRTP)
Suche…
Einführung
Ein Muster, in dem eine Klasse von einer Klassenvorlage mit sich selbst als einem ihrer Vorlagenparameter erbt. CRTP wird normalerweise verwendet, um statischen Polymorphismus in C ++ bereitzustellen.
Das kurioserweise wiederkehrende Vorlagenmuster (CRTP)
CRTP ist eine leistungsstarke, statische Alternative zu virtuellen Funktionen und herkömmlicher Vererbung, mit der Typeneigenschaften zur Kompilierzeit angegeben werden können. Es verfügt über eine Basisklassenvorlage, die die abgeleitete Klasse als einen ihrer Vorlagenparameter übernimmt. Dies erlaubt es ihm, legal einen static_cast
seines this
Zeigers auf die abgeleitete Klasse durchzuführen.
Das bedeutet natürlich auch, dass eine CRTP-Klasse immer als Basisklasse einer anderen Klasse verwendet werden muss. Und die abgeleitete Klasse muss sich selbst an die Basisklasse übergeben.
Nehmen wir an, Sie haben eine Reihe von Containern, die alle die Funktionen begin()
und end()
. Die Anforderungen der Standardbibliothek für Container erfordern mehr Funktionalität. Wir können eine CRTP-Basisklasse entwerfen, die diese Funktionalität ausschließlich auf der Grundlage von begin()
und end()
bereitstellt:
#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);
}
};
Die obige Klasse stellt die Funktionen front()
, back()
, size()
und operator[]
für jede Unterklasse bereit, die begin()
und end()
bereitstellt. Eine beispielhafte Unterklasse ist ein einfaches, dynamisch zugewiesenes Array:
#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_;
};
Benutzer der DynArray
Klasse können die von der CRTP-Basisklasse bereitgestellten Schnittstellen problemlos wie folgt verwenden:
DynArray<int> arr(10);
arr.front() = 2;
arr[2] = 5;
assert(arr.size() == 10);
Nützlichkeit: Dieses Muster vermeidet insbesondere virtuelle Funktionsaufrufe zur Laufzeit, die zum Durchlaufen der Vererbungshierarchie auftreten und stützen sich einfach auf statische Umsetzungen:
DynArray<int> arr(10);
DynArray<int>::Base & base = arr;
base.begin(); // no virtual calls
Die einzige statische Container<DynArray<int>>
innerhalb der Funktion begin()
in der Basisklasse Container<DynArray<int>>
ermöglicht es dem Compiler, den Code drastisch zu optimieren, und zur Laufzeit erfolgt keine virtuelle Tabellensuche.
Einschränkungen: Da die Basisklasse für zwei verschiedene DynArray
Klassen unterschiedlich DynArray
, ist es nicht möglich, Zeiger auf ihre Basisklassen in einem typhomogenen Array zu speichern, wie dies bei normaler Vererbung der DynArray
wäre, bei der die Basisklasse nicht von der abgeleiteten Klasse abhängt Art:
class A {};
class B: public A{};
A* a = new B;
CRTP, um Code-Duplizierung zu vermeiden
Das Beispiel in Visitor Pattern bietet einen überzeugenden Anwendungsfall für 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); }
// ...
};
Jeder IShape
Typ von IShape
muss dieselbe Funktion auf dieselbe Weise implementieren. Das ist viel mehr Tippen. Stattdessen können wir einen neuen Typ in der Hierarchie einführen, der dies für uns tut:
template <class Derived>
struct IShapeAcceptor : IShape {
void accept(IShapeVisitor& visitor) const override {
// visit with our exact type
visitor.visit(*static_cast<Derived const*>(this));
}
};
Und jetzt muss jede Form einfach vom Akzeptor erben:
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;
};
Kein doppelter Code erforderlich.