C++
Любопытно повторяющийся шаблон шаблона (CRTP)
Поиск…
Вступление
Шаблон, в котором класс наследует шаблон шаблона сам по себе как один из его параметров шаблона. CRTP обычно используется для обеспечения статического полиморфизма в C ++.
Любопытно повторяющийся шаблон шаблона (CRTP)
CRTP - это мощная статическая альтернатива виртуальным функциям и традиционному наследованию, которые могут использоваться для предоставления свойств типов во время компиляции. Он работает, имея шаблон базового класса, который принимает в качестве одного из своих параметров шаблона производный класс. Это позволяет ему юридически выполнить static_cast
его this
указателя на производный класс.
Разумеется, это также означает, что класс CRTP всегда должен использоваться как базовый класс другого класса. И производный класс должен перейти к базовому классу.
Допустим, у вас есть набор контейнеров, которые поддерживают функции begin()
и end()
. Требования к стандартной библиотеке для контейнеров требуют большей функциональности. Мы можем создать базовый класс CRTP, который обеспечивает эту функциональность, основанную исключительно на begin()
и 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);
}
};
Вышеупомянутый класс предоставляет функции front()
, back()
, size()
и operator[]
для любого подкласса, который предоставляет begin()
и end()
. Примером подкласса является простой динамически распределенный массив:
#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_;
};
Пользователи класса DynArray
могут легко использовать интерфейсы, предоставляемые базовым классом CRTP, следующим образом:
DynArray<int> arr(10);
arr.front() = 2;
arr[2] = 5;
assert(arr.size() == 10);
Полезность. Этот шаблон, в частности, избегает вызовов виртуальных функций во время выполнения, которые происходят, чтобы пересечь иерархию наследования и просто полагается на статические приведения:
DynArray<int> arr(10);
DynArray<int>::Base & base = arr;
base.begin(); // no virtual calls
Единственный статический бросок внутри функции begin()
в базовом классе Container<DynArray<int>>
позволяет компилятору радикально оптимизировать код, и при просмотре виртуальной таблицы не происходит во время выполнения.
Ограничения: поскольку базовый класс шаблонизирован и отличается для двух разных DynArray
s, невозможно сохранить указатели на их базовые классы в однородном по типу массиве, как обычно можно было бы сделать с обычным наследованием, когда базовый класс не зависит от производного тип:
class A {};
class B: public A{};
A* a = new B;
CRTP, чтобы избежать дублирования кода
Пример в шаблоне посетителей представляет собой убедительный прецедент для 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); }
// ...
};
Каждому дочернему типу IShape
необходимо реализовать ту же функцию одинаково. Это много лишнего набора текста. Вместо этого мы можем ввести новый тип в иерархию, который делает это для нас:
template <class Derived>
struct IShapeAcceptor : IShape {
void accept(IShapeVisitor& visitor) const override {
// visit with our exact type
visitor.visit(*static_cast<Derived const*>(this));
}
};
И теперь каждая форма просто должна унаследовать от акцептора:
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;
};
Не требуется дублировать код.