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.

C ++ 14

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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow