Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
47
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

Chapter 79: Curiously Recurring Template

Pattern (CRTP)

A pattern in which a class inherits from a class template with itself as one of its template parameters. CRTP is usually used to provide static polymorphism in C++.

Section 79.1: The Curiously Recurring Template Pattern (CRTP)

CRTP is a powerful, static alternative to virtual functions and traditional inheritance that can be used to give types properties at compile time. It works by having a base class template which takes, as one of its template parameters, the derived class. This permits it to legally perform a static_cast of its this pointer to the derived class.

Of course, this also means that a CRTP class must always be used as the base class of some other class. And the derived class must pass itself to the base class.

Version ≥ C++14

Let's say you have a set of containers that all support the functions begin() and end(). The standard library's requirements for containers require more functionality. We can design a CRTP base class that provides that functionality, based solely on begin() and 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);

}

};

The above class provides the functions front(), back(), size(), and operator[] for any subclass which provides begin() and end(). An example subclass is a simple dynamically allocated array:

#include <memory>

// A dynamically allocated array template <typename T>

class DynArray : public Container<DynArray<T>> { public:

GoalKicker.com – C++ Notes for Professionals

429

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_;

};

Users of the DynArray class can use the interfaces provided by the CRTP base class easily as follows:

DynArray<int> arr(10); arr.front() = 2; arr[2] = 5;

assert(arr.size() == 10);

Usefulness: This pattern particularly avoids virtual function calls at run-time which occur to traverse down the inheritance hierarchy and simply relies on static casts:

DynArray<int> arr(10); DynArray<int>::Base & base = arr; base.begin(); // no virtual calls

The only static cast inside the function begin() in the base class Container<DynArray<int>> allows the compiler to drastically optimize the code and no virtual table look up happens at runtime.

Limitations: Because the base class is templated and di erent for two di erent DynArrays it is not possible to store pointers to their base classes in an type-homogenous array as one could generally do with normal inheritance where the base class is not dependent on the derived type:

class A {};

class B: public A{};

A* a = new B;

Section 79.2: CRTP to avoid code duplication

The example in Visitor Pattern provides a compelling use-case for 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

GoalKicker.com – C++ Notes for Professionals

430

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); } // ...

};

Each child type of IShape needs to implement the same function the same way. That's a lot of extra typing. Instead,

we can introduce a new type in the hierarchy that does this for us:

template <class Derived>

struct IShapeAcceptor : IShape {

void accept(IShapeVisitor& visitor) const override { // visit with our exact type visitor.visit(*static_cast<Derived const*>(this));

}

};

And now, each shape simply needs to inherit from the 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;

};

No duplicate code necessary.

GoalKicker.com – C++ Notes for Professionals

431