C++
Merkwaardig terugkerend sjabloonpatroon (CRTP)
Zoeken…
Invoering
Een patroon waarin een klasse van een klassensjabloon overneemt met zichzelf als een van de sjabloonparameters. CRTP wordt meestal gebruikt om statisch polymorfisme in C ++ te verschaffen.
Het merkwaardig terugkerende sjabloonpatroon (CRTP)
CRTP is een krachtig, statisch alternatief voor virtuele functies en traditionele overerving die kan worden gebruikt om typen eigenschappen te geven tijdens het compileren. Het werkt door een basisklassjabloon te hebben die, als een van de sjabloonparameters, de afgeleide klasse aanneemt. Dit staat het toe om legaal een static_cast
van zijn this
wijzer naar de afgeleide klasse uit te voeren.
Dit betekent natuurlijk ook dat een CRTP-klasse altijd moet worden gebruikt als de basisklasse van een andere klasse. En de afgeleide klasse moet zichzelf doorgeven aan de basisklasse.
Stel dat u een set containers hebt die allemaal de functies begin()
en end()
. De vereisten van de standaardbibliotheek voor containers vereisen meer functionaliteit. We kunnen een CRTP-basisklasse ontwerpen die die functionaliteit biedt, uitsluitend gebaseerd op begin()
en 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);
}
};
De bovenstaande klasse biedt de functies front()
, back()
, size()
en operator[]
voor elke subklasse die begin()
en end()
. Een voorbeeldsubklasse is een eenvoudige dynamisch toegewezen 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_;
};
Gebruikers van de DynArray
klasse kunnen de interfaces van de CRTP-basisklasse eenvoudig als volgt gebruiken:
DynArray<int> arr(10);
arr.front() = 2;
arr[2] = 5;
assert(arr.size() == 10);
Nuttigheid: dit patroon vermijdt met name virtuele functieaanroepen tijdens runtime die zich voordoen om door de overervingshiërarchie te bladeren en vertrouwt eenvoudig op statische cast:
DynArray<int> arr(10);
DynArray<int>::Base & base = arr;
base.begin(); // no virtual calls
Met de enige statische cast in de functie begin()
in de basisklasse Container<DynArray<int>>
kan de compiler de code drastisch optimaliseren en wordt er tijdens de uitvoering geen virtuele tabel Container<DynArray<int>>
.
Beperkingen: Omdat de basisklasse sjablonen is en verschillend voor twee verschillende DynArray
's, is het niet mogelijk om verwijzingen naar hun basisklassen op te slaan in een type-homogene array, zoals men gewoonlijk zou kunnen doen met normale overerving waarbij de basisklasse niet afhankelijk is van de afgeleide type:
class A {};
class B: public A{};
A* a = new B;
CRTP om codeduplicatie te voorkomen
Het voorbeeld in bezoekerspatroon biedt een overtuigende use-case voor 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); }
// ...
};
Elk IShape
van IShape
moet dezelfde functie op dezelfde manier implementeren. Dat is veel extra typen. In plaats daarvan kunnen we een nieuw type in de hiërarchie introduceren dat dit voor ons doet:
template <class Derived>
struct IShapeAcceptor : IShape {
void accept(IShapeVisitor& visitor) const override {
// visit with our exact type
visitor.visit(*static_cast<Derived const*>(this));
}
};
En nu moet elke vorm gewoon erven van de acceptor:
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;
};
Geen dubbele code nodig.