提问人:Alok Save 提问时间:11/13/2010 最后编辑:TankorSmashAlok Save 更新时间:9/13/2023 访问量:67724
什么是奇怪的重复模板模式 (CRTP)?
What is the curiously recurring template pattern (CRTP)?
答:
简而言之,CRTP 是指一个类具有基类,该基类是类本身的模板专用化。例如A
A
template <class T>
class X{...};
class A : public X<A> {...};
它奇怪地反复出现,不是吗?:)
现在,这给你带来了什么?这实际上使模板能够成为其专用化的基类。X
例如,您可以像这样创建一个通用单例类(简化版本)
#include <iostream>
template <class T>
class Singleton
{
public:
static T* GetInstance() {
if ( p == nullptr ) p = new T();
return p;
}
protected:
Singleton() = default;
Singleton(Singleton const &) = delete;
Singleton &operator=(const Singleton &) = delete;
private:
static T *p;
};
template <class T>
T *Singleton<T>::p= nullptr;
现在,为了使任意类成为单例,您应该这样做A
class A : public Singleton<A>
{
friend Singleton;
private:
A() = default;
};
A *a0= A::GetInstance();
在这种情况下,不需要 CRTP,如下所示:
class C
{
friend Singleton<C>;
private: C() = default;
};
C *c1= Singleton<C>::GetInstance();
所以你明白了吗?单一实例模板假定其对任何类型的专用化都将继承自,因此其所有(公共、受保护)成员都可以访问,包括 !CRTP 还有其他有用的用途。例如,如果你想计算你的类当前存在的所有实例,但想将这个逻辑封装在一个单独的模板中(具体类的想法很简单 - 有一个静态变量,在 ctors 中递增,在 dtors 中递减)。试着把它当作一种练习来做!X
singleton<X>
GetInstance
另一个有用的例子,对于 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 <
}
或在模板范围内实现,无需强制转换
template<class T>
class Equality
{
friend bool operator == (const T& op1, const T& op2)
{
return !(op1 < op2) && !(op2 < op1);
}
};
现在你可以像这样使用它了
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可用于实现静态多态性(与动态多态性类似,但没有虚函数指针表)。
#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
vtable
Base<>::method ()
methodImpl
method
Base
methodImpl
method
在这里,你可以看到一个很好的例子。如果使用虚拟方法,程序将知道在运行时执行什么。实现 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
这不是一个直接的答案,而是 CRTP 如何有用的一个示例。
CRTP 的一个很好的具体示例来自 C++11:std::enable_shared_from_this
类可以继承 to 继承成员函数,这些成员函数获取指向 的实例。
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 是一种实现编译时多态性的技术。这是一个非常简单的例子。在下面的示例中,使用类接口并调用派生对象的方法,这就是您打算使用虚拟方法执行的操作。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
评论
ProcessFoo()
void ProcessFoo(T* b)
ProcessFoo()
foo()
ProcessFoo
使用 CRTP 的另一个很好的例子是观察者设计模式的实现。一个小例子可以这样构建。
假设你有一个类,并且你有一些侦听器类,如 、 等。侦听器类(观察者)
每当日期更改完成时,应由主题类(可观察)通知,以便他们可以完成他们的工作(在某些中绘制日期
格式,提醒特定日期等)。您可以做的是有两个参数化基类,您应该从中派生
your 和 observer 类(在我们的例子中)。有关观察者设计模式的实现,请参考 GOF 等经典书籍。在这里,我们只需要
突出显示 CRTP 的使用。让我们来看看它。
在我们的实现草案中,基类有一个纯虚方法,每当发生状态更改时,类都应该调用它,
我们称之为 此方法 。让我们看一下这个小抽象基类的代码。date
date_drawer
date_reminder
date
observer
observable
date
date_drawer
observer
observable
state_changed
template <typename T>
struct observer
{
virtual void state_changed(T*, variant<string, int, bool>) = 0;
virtual ~observer() {}
};
在这里,我们应该关注的主要参数是第一个参数,它将成为更改状态的对象。第二个参数
将是被更改的字段,它可以是任何东西,甚至可以省略它,这不是我们主题的问题(在这种情况下,它是
3 个字段)。
第二个基类是T*
std::variant
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)
{
}
};
它也是一个依赖于类型的参数类,它与传递给函数内部函数的对象相同。
剩下的只是介绍实际的主体类和观察者类。这里使用 CRTP 模式,我们从 observable<date>:
class date : public observable<date>
派生日期可观察类。T*
state_changed
notify_observers
date
date_drawer
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 并在发生状态更改时传递给通知函数(和此处)。允许我们在实际的观察者类中覆盖纯虚函数时使用,因此我们在它里面有 (不是 ),例如我们可以调用 (在我们的例子中) 的非虚函数
在函数中。
我们可以避免使用 CRTP,因此不参数化观察者设计模式实现,而是在任何地方使用基类指针。这样我们就可以达到同样的效果,但在这种情况下,每当我们想使用派生类指针时(即使不是很推荐),我们都应该使用下沉,它有一些运行时开销。this
notify_observers
set_date_properties
set_date
date*
void state_changed(date* c, variant<string, int, bool> state)
date_drawer
date* c
observable*
date*
get_date
state_changed
observable
dynamic_cast
下一个:特定成员的模板专业化?
评论