Что такое любопытно повторяющийся шаблон (CRTP)?
Не обращаясь к книге, кто-нибудь может дать хорошее объяснение CRTP
с примером кода?
6 ответов
Короче говоря, CRTP - это когда класс А имеет базовый класс, который является специализацией шаблона для самого класса А. Например
template <class T>
class X{...};
class A : public X<A> {...};
Это любопытно повторяется, не так ли?:)
Теперь, что это дает вам? Это фактически дает шаблону X возможность быть базовым классом для его специализаций.
Например, вы можете создать общий синглтон-класс (упрощенную версию), например так:
template <class ActualClass>
class Singleton
{
public:
static ActualClass& GetInstance()
{
if(p == nullptr)
p = new ActualClass;
return *p;
}
protected:
static ActualClass* p;
private:
Singleton(){}
Singleton(Singleton const &);
Singleton& operator = (Singleton const &);
};
template <class T>
T* Singleton<T>::p = nullptr;
Теперь, чтобы сделать произвольный класс А синглтоном, вы должны сделать это
class A: public Singleton<A>
{
//Rest of functionality for class A
};
Так ты видишь? Шаблон Singleton предполагает, что его специализация для любого типа X будет унаследована от singleton<X>
и, таким образом, будут доступны все его (публичные, защищенные) члены, включая GetInstance
! Есть и другие полезные применения CRTP. Например, если вы хотите подсчитать все экземпляры, которые в данный момент существуют для вашего класса, но хотите инкапсулировать эту логику в отдельный шаблон (идея для конкретного класса довольно проста - иметь статическую переменную, инкремент в ctors, декремент в dtors). Попробуйте сделать это как упражнение!
Еще один полезный пример, для повышения (я не уверен, как они осуществили это, но CRTP сделает то же самое). Представьте, что вы хотите предоставить только operator <для ваших классов, но автоматически operator == для них!
Вы можете сделать это так:
template<class Derived>
class Equality
{
};
template <class Derived>
bool operator == (Equality<Derived> const& op1, Equality<Derived> const & op2)
{
Derived const& d1 = static_cast<Derived const&>(op1);//you assume this works
//because you know that the dynamic type will actually be your template parameter.
//wonderful, isnit it?
Derived const& d2 = static_cast<Derived const&>(op2);
return !(d1 < d2) && !(d2 < d1);//assuming derived has operator <
}
Теперь вы можете использовать это так
struct Apple:public Equality<Apple>
{
int size;
};
bool operator < (Apple const & a1, Apple const& a2)
{
return a1.size < a2.size;
}
Теперь, вы не предоставили явно оператор == для Apple? Но у вас есть это! Ты можешь написать
int main()
{
Apple a1;
Apple a2;
a1.size = 10;
a2.size = 10;
if(a1 == a2) //the compiler won't complain!
{
}
}
Может показаться, что вы написали бы меньше, если бы просто написали оператор == для Apple, но представьте, что шаблон Equality будет обеспечивать не только ==, но>, >=, <= и т. Д. И вы можете использовать эти определения для нескольких классов, повторное использование кода!
CRTP замечательная вещь:) HTH
Здесь вы можете увидеть отличный пример. Если вы используете виртуальный метод, программа будет знать, что выполнить во время выполнения. Реализация CRTP - это компилятор, который решает во время компиляции!!! Это отличное представление!
template <class T>
class Writer
{
public:
Writer() { }
~Writer() { }
void write(const char* str) const
{
static_cast<const T*>(this)->writeImpl(str); //here the magic is!!!
}
};
class FileWriter : public Writer<FileWriter>
{
public:
FileWriter(FILE* aFile) { mFile = aFile; }
~FileWriter() { fclose(mFile); }
//here comes the implementation of the write method on the subclass
void writeImpl(const char* str) const
{
fprintf(mFile, "%s\n", str);
}
private:
FILE* mFile;
};
class ConsoleWriter : public Writer<ConsoleWriter>
{
public:
ConsoleWriter() { }
~ConsoleWriter() { }
void writeImpl(const char* str) const
{
printf("%s\n", str);
}
};
CRTP - это метод реализации полиморфизма во время компиляции. Вот очень простой пример. В приведенном ниже примере ProcessFoo()
работает с Base
интерфейс класса и Base::Foo
вызывает производный объект foo()
Метод, который вы хотите сделать с виртуальными методами.
http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e
template <typename T>
struct Base {
void foo() {
(static_cast<T*>(this))->foo();
}
};
struct Derived : public Base<Derived> {
void foo() {
cout << "derived foo" << endl;
}
};
struct AnotherDerived : public Base<AnotherDerived> {
void foo() {
cout << "AnotherDerived foo" << endl;
}
};
template<typename T>
void ProcessFoo(Base<T>* b) {
b->foo();
}
int main()
{
Derived d1;
AnotherDerived d2;
ProcessFoo(&d1);
ProcessFoo(&d2);
return 0;
}
Выход:
derived foo
AnotherDerived foo
Это не прямой ответ, а пример того, как CRTP может быть полезен.
Хорошим конкретным примером CRTP является std::enable_shared_from_this
из C++ 11:
Класс
T
может наследовать отenable_shared_from_this<T>
унаследоватьshared_from_this
функции-члены, которые получаютshared_ptr
экземпляр, указывающий на*this
,
То есть наследуя от std::enable_shared_from_this
позволяет получить общий (или слабый) указатель на ваш экземпляр без доступа к нему (например, из функции-члена, где вы знаете только о *this
).
Это полезно, когда вам нужно дать std::shared_ptr
но у вас есть доступ только к *this
:
struct Node;
void process_node(const std::shared_ptr<Node> &);
struct Node : std::enable_shared_from_this<Node> // CRTP
{
std::weak_ptr<Node> parent;
std::vector<std::shared_ptr<Node>> children;
void add_child(std::shared_ptr<Node> child)
{
process_node(shared_from_this()); // Shouldn't pass `this` directly.
child->parent = weak_from_this(); // Ditto.
children.push_back(std::move(child));
}
};
Причина, по которой вы не можете просто пройти this
прямо вместо shared_from_this()
в том, что это сломало бы механизм владения:
struct S
{
std::shared_ptr<S> get_shared() const { return std::shared_ptr<S>(this); }
};
// Both shared_ptr think they're the only owner of S.
// This invokes UB (double-free).
std::shared_ptr<S> s1 = std::make_shared<S>();
std::shared_ptr<S> s2 = s1->get_shared();
assert(s2.use_count() == 1);
Так же, как примечание:
CRTP может быть использован для реализации статического полиморфизма (который похож на динамический полиморфизм, но без таблицы указателей виртуальных функций).
#pragma once
#include <iostream>
template <typename T>
class Base
{
public:
void method() {
static_cast<T*>(this)->method();
}
};
class Derived1 : public Base<Derived1>
{
public:
void method() {
std::cout << "Derived1 method" << std::endl;
}
};
class Derived2 : public Base<Derived2>
{
public:
void method() {
std::cout << "Derived2 method" << std::endl;
}
};
#include "crtp.h"
int main()
{
Derived1 d1;
Derived2 d2;
d1.method();
d2.method();
return 0;
}
Выход будет:
Derived1 method
Derived2 method
Другим хорошим примером использования CRTP может быть реализация шаблона проектирования наблюдателя. Небольшой пример можно построить так.
Предположим, у вас есть класс и несколько классов слушателей, например,
date_reminder
и т. д. Классы слушателей (наблюдатели) должны быть уведомлены классом субъекта (наблюдаемым) всякий раз, когда происходит изменение даты, чтобы они могли выполнять свою работу (нарисовать дату в каком-либо формате, напомнить об определенной дате и т. д.). Что вы можете сделать, так это иметь два параметризованных базовых класса, из которых вы должны получить свои классы и классы-наблюдатели (в нашем случае). Для реализации шаблона проектирования наблюдателя обратитесь к классическим книгам, таким как GOF. Здесь нам нужно только выделить использование CRTP. Давайте посмотрим на это. В нашем проекте реализации
observer
базовый класс имеет один чисто виртуальный метод, который должен вызываться классом всякий раз, когда происходит изменение состояния, давайте назовем этот метод. Давайте посмотрим на код этого небольшого абстрактного базового класса.
template <typename T>
struct observer
{
virtual void state_changed(T*, variant<string, int, bool>) = 0;
virtual ~observer() {}
};
Здесь основной параметр, на который мы должны обратить внимание, — это первый аргумент, который будет объектом, для которого было изменено состояние. Вторым параметром будет поле, которое было изменено, оно может быть любым, даже его можно опустить, это не проблема нашей темы (в данном случае это
std::variant
из 3-х полей). Второй базовый класс
template <typename T>
class observable
{
vector<unique_ptr<observer<T>>> observers;
protected:
void notify_observers(T* changed_obj, variant<string, int, bool> changed_state)
{
for (unique_ptr<observer<T>>& o : observers)
{
o->state_changed(changed_obj, changed_state);
}
}
public:
void subscribe_observer(unique_ptr<observer<T>> o)
{
observers.push_back(move(o));
}
void unsubscribe_observer(unique_ptr<observer<T>> o)
{
}
};
который также является параметрическим классом, который зависит от типа
T*
и это тот же объект, который передается функции внутри функции. Остается только ввести собственно класс субъекта и класс наблюдателя. Здесь используется шаблон CRTP, мы выводим
date
наблюдаемый класс из
observable<date>
:
class date : public observable<date>
.
class date : public observable<date>
{
string date_;
int code;
bool is_bank_holiday;
public:
void set_date_properties(int code_ = 0, bool is_bank_holiday_ = false)
{
code = code_;
is_bank_holiday = is_bank_holiday_;
//...
notify_observers(this, code);
notify_observers(this, is_bank_holiday);
}
void set_date(const string& new_date, int code_ = 0, bool is_bank_holiday_ = false)
{
date_ = new_date;
//...
notify_observers(this, new_date);
}
string get_date() const { return date_; }
};
class date_drawer : public observer<date>
{
public:
void state_changed(date* c, variant<string, int, bool> state) override
{
visit([c](const auto& x) {cout << "date_drawer notified, new state is " << x << ", new date is " << c->get_date() << endl; }, state);
}
};
Давайте напишем клиентский код:
date c;
c.subscribe_observer(make_unique<date_drawer>());
c.set_date("27.01.2022");
c.set_date_properties(7, true);
вывод этой тестовой программы будет.
date_drawer notified, new state is 27.01.2022, new date is 27.01.2022
date_drawer notified, new state is 7, new date is 27.01.2022
date_drawer notified, new state is 1, new date is 27.01.2022
Обратите внимание, что использование CRTP и передача
this
уведомить
notify_observers
функционировать всякий раз, когда происходит изменение состояния (
set_date_properties
а также
set_date
здесь). Разрешено использовать при переопределении
void state_changed(date* c, variant<string, int, bool> state)
чисто виртуальная функция в реальном
date_drawer
класс наблюдателя, следовательно, мы имеем
date* c
внутри него (не
observable*
) и, например, мы можем вызвать не виртуальную функцию
date*
(
get_date
в нашем случае) внутри
state_changed
функция. Мы могли бы воздержаться от желания использовать CRTP и, следовательно, не параметризовать реализацию и использование шаблона проектирования наблюдателя.
observable
указатель базового класса везде. Таким образом, мы могли бы получить тот же эффект, но в этом случае всякий раз, когда мы хотим использовать указатель производного класса (даже если это не очень рекомендуется), мы должны использовать
dynamic_cast
downcasting, который имеет некоторые накладные расходы во время выполнения.