C++
Ciekawie powtarzający się wzór szablonu (CRTP)
Szukaj…
Wprowadzenie
Wzorzec, w którym klasa dziedziczy po szablonie klasy, który jest jednym z parametrów szablonu. CRTP jest zwykle używany do zapewnienia statycznego polimorfizmu w C ++.
Ciekawie powtarzający się wzorzec szablonu (CRTP)
CRTP jest potężną, statyczną alternatywą dla funkcji wirtualnych i tradycyjnego dziedziczenia, której można użyć do nadania właściwościom typów w czasie kompilacji. Działa, mając szablon klasy bazowej, który przyjmuje, jako jeden z parametrów szablonu, klasę pochodną. Pozwala to na legalne wykonanie static_cast
jego this
wskaźnika do klasy pochodnej.
Oczywiście oznacza to również, że klasa CRTP musi zawsze być używana jako klasa bazowa niektórych innych klas. Klasa pochodna musi przejść do klasy podstawowej.
Załóżmy, że masz zestaw kontenerów, które obsługują wszystkie funkcje begin()
i end()
. Wymagania biblioteki standardowej dla kontenerów wymagają większej funkcjonalności. Możemy zaprojektować klasę bazową CRTP, która zapewnia tę funkcjonalność, wyłącznie na podstawie begin()
i 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);
}
};
Powyższa klasa udostępnia funkcje front()
, back()
, size()
i operator[]
dla każdej podklasy, która udostępnia begin()
i end()
. Przykładową podklasą jest prosta tablica dynamicznie przydzielana:
#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_;
};
Użytkownicy klasy DynArray
mogą łatwo korzystać z interfejsów dostarczonych przez klasę podstawową CRTP w następujący sposób:
DynArray<int> arr(10);
arr.front() = 2;
arr[2] = 5;
assert(arr.size() == 10);
Przydatność: ten wzorzec szczególnie unika wirtualnych wywołań funkcji w czasie wykonywania, które występują w celu przejścia w dół hierarchii dziedziczenia i po prostu opiera się na rzutach statycznych:
DynArray<int> arr(10);
DynArray<int>::Base & base = arr;
base.begin(); // no virtual calls
Jedyny rzut statyczny wewnątrz funkcji begin()
w klasie bazowej Container<DynArray<int>>
pozwala kompilatorowi drastycznie zoptymalizować kod i żadne wyszukiwanie tabel wirtualnych nie występuje w czasie wykonywania.
Ograniczenia: Ponieważ klasa podstawowa jest szablonowana i różni się dla dwóch różnych DynArray
nie jest możliwe przechowywanie wskaźników do ich klas podstawowych w tablicy jednorodnej pod względem typu, co można ogólnie zrobić w przypadku normalnego dziedziczenia, w którym klasa podstawowa nie jest zależna od pochodnej rodzaj:
class A {};
class B: public A{};
A* a = new B;
CRTP, aby uniknąć powielania kodu
Przykład we wzorcu użytkownika przedstawia przekonujący przypadek użycia dla 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); }
// ...
};
Każdy typ IShape
musi implementować tę samą funkcję w ten sam sposób. To dużo dodatkowego pisania. Zamiast tego możemy wprowadzić nowy typ w hierarchii, który robi to za nas:
template <class Derived>
struct IShapeAcceptor : IShape {
void accept(IShapeVisitor& visitor) const override {
// visit with our exact type
visitor.visit(*static_cast<Derived const*>(this));
}
};
A teraz każdy kształt musi po prostu dziedziczyć po akceptorze:
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;
};
Nie jest wymagany duplikat kodu.