Не обращаясь к книге, может ли кто-нибудь дать хорошее объяснение CRTP
на примере кода?
Что такое необычно повторяющийся шаблонный шаблон (CRTP)?
Ответы (5)
Короче говоря, CRTP - это когда класс A
имеет базовый класс, который является специализацией шаблона для самого класса A
. Например.
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;
Теперь, чтобы сделать произвольный класс A
синглтоном, вы должны сделать это
class A: public Singleton<A>
{
//Rest of functionality for class A
};
Так ты видишь? Шаблон синглтона предполагает, что его специализация для любого типа X
будет унаследована от singleton<X>
и, таким образом, будет иметь доступ ко всем его (общедоступным, защищенным) членам, включая GetInstance
! Есть и другие полезные применения CRTP. Например, если вы хотите подсчитать все экземпляры, которые в настоящее время существуют для вашего класса, но хотите инкапсулировать эту логику в отдельный шаблон (идея для конкретного класса довольно проста - иметь статическую переменную, увеличивать в ctors, уменьшать в dtors ). Попробуйте сделать это как упражнение!
Еще один полезный пример для Boost (я не уверен, как они его реализовали, но CRTP тоже подойдет). Представьте, что вы хотите предоставить только оператор <
для своих классов, но автоматически оператор ==
для них!
вы могли бы сделать это так:
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, isn't 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
предоставит не только ==
, но >
, >=
, <=
и т. Д. И вы можете использовать эти определения для множественных em > классы, повторно используя код!
CRTP - замечательная штука :) HTH
class A
несколькими свойствами, например, я хочу, чтобы A
был одноэлементным и имел operator==
. Разве предложенная стратегия не приведет меня к множественному наследованию со всеми вытекающими отсюда оговорками?
- person Michael; 05.01.2017
Singleton()
и других 3 функций на protected
и измените доступ ActualClass* p
на private
.
- person where23; 10.07.2019
Вот отличный пример. Если вы используете виртуальный метод, программа будет знать, что выполняется во время выполнения. Реализуя 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);
}
};
virtual void write(const char* str) const = 0;
? Хотя, честно говоря, этот метод кажется очень полезным, когда write
выполняет другую работу.
- person atlex2; 09.08.2016
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
foo()
производным классом.
- person blueskin; 04.06.2018
ProcessFoo()
.
- person Pietro; 11.09.2018
void ProcessFoo(T* b)
и без фактического извлечения Derived и AnotherDerived он все равно будет работать. ИМХО было бы интереснее, если бы ProcessFoo как-то не использовал шаблоны.
- person Gabriel Devillers; 04.06.2020
ProcessFoo()
будет работать с любым типом, реализующим интерфейс, т.е. в этом случае тип ввода T должен иметь метод с именем foo()
. Во-вторых, чтобы заставить нешаблонный ProcessFoo
работать с несколькими типами, вы, скорее всего, в конечном итоге будете использовать RTTI, чего мы хотим избежать. Кроме того, в шаблонной версии вы можете проверить время компиляции в интерфейсе.
- person blueskin; 12.06.2020
Это не прямой ответ, а скорее пример того, как 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
vtable
без использования CRTP. Что действительно предоставляет vtable
s, так это использование базового класса (указателя или ссылки) для вызова производных методов. Здесь вы должны показать, как это делается с помощью CRTP.
- person Etherealone; 06.02.2014
Base<>::method ()
даже не вызывается, и вы нигде не используете полиморфизм.
- person MikeMB; 12.03.2015
methodImpl
в method
Base
и в производных классах имя methodImpl
вместо method
- person Ivan Kush; 17.09.2016