奇异递归模板模式(Curiously Recurring Template Pattern,CRTP)是C++模板编程时的一种惯用法(idiom):把派生类作为基类的模板参数。更一般地被称作F-bound polymorphism。CRTP在C++中主要有两种用途:
- 静态多态(static polymorphism)
- 添加方法,同时精简代码
本文将会对CRTP,进行比较详细的讲解。
CRTP基础
动态多态
C++动态多态,也就是运行时多态。这是利用继承、虚函数、基类的指针指向派生类的对象,来实现的。一个简单的例子:
#include <iostream>
class Base {
public:
virtual void Run() { std::cout << "Base::Run()"; }
};
class Derived : public Base {
public:
void Run() { std::cout << "Derived::Run()"; }
};
int main()
{
Base *ptr = new Derived();
ptr->Run();
return 0;
}
此时,Run()函数的输出结果是Derived::Run()。
这就是动态多态,是通过虚指针 -> 虚函数表 -> 虚函数,实现的。在性能上,会有额外的开销。
静态多态
所谓静态多态,是指CRTP通过派生类对基类模板实例化,也可以实现类似动态多态的效果。最鲜明的特点就是:
- 继承自模板类;
- 使用派生类作为模板参数特化基类;
一个简单的例子:
#include <iostream>
template <typename T>
class Base {
public:
void Run() { static_cast<T*>(this)->Run(); }
};
class Derived : public Base<Derived> {
public:
void Run() { std::cout << "Derived::Run()"; }
};
int main()
{
Base<Derived> *ptr = new Derived();
ptr->Run();
return 0;
}
此时,Run()函数的输出结果是Derived::Run()。
有时候为了更简化代码,可以修改成:
template <typename T>
class Base {
public:
T* cast() { return static_cast<T*>(this); }
void Run() { cast()->Run(); }
};
class Derived : public Base<Derived> {
public:
void Run() { std::cout << "Derived::Run()"; }
};
这样,每个派生类都可以实现各自的Run()方法,最终由于指针是基类的指针,并且没有实现虚函数。因此,调用的都是基类的Run()方法。但是基类的Run()方法中,都将指针转为了派生类。最终调用的都是各自派生类的Run()方法。
使用派生类作为模板参数特化基类,这样做的目的是为了基类中使用派生类,从基类的角度来看,派生类其实也是基类,通过向下转换[downcast]。因此,基类可以通过static_cast把其转换到派生类,从而使用派生类的成员,形式如下:
T* derived = static_cast<T*>(this);
T& derived = static_cast<T&>(*this);
这两种方式都是可以使用的。
注意:这里不使用dynamic_cast,因为dynamic_cast一般是为了确保在运行期(run-time)向上向下转换的正确性。CRTP的设计是:派生类就是基类的模板参数,因此static_cast足矣。
静态多态实现原理:编译时编译器会为模板生成一份实例化代码,根据对应实例调用对应函数。
静态多态与和动态的区别是:多态是动态绑定(运行时绑定run-time binding),CRTP是静态绑定(编译时绑定 compile-time binding)。
其中,动态多态在实现多态时,需要重写虚函数,这种运行时绑定的操作往往需要查找虚表等,效率低。而template的核心技术在于编译期多态机制,与运行期多态相比,这种机制提供编译期多态性,给了程序运行期无可比拟的效率优势。
关于编译期编译期多态和运行期多态效率对比,可以参考文章:The cost of dynamic (virtual calls) vs. static (CRTP) dispatch in C++。
虽然静态多态避免了动态多态的性能开销问题。但是每个模板实例会在编译时生成一份实例化代码,如果使用大量的模板可能会导致 代码膨胀。
CRTP应用
粗略地了解了CRTP的原理和代码基本范式,下面会对几个典型的CRTP的使用场景和应用进行分析,以便更好地理解。
std::enable_shared_from_this
在shared_ptr的学习中,我们会被强调一点:不要将this指针返回给shared_ptr。这是因为,在返回this的sharedptr时,又通过this指针构造了一个shared_ptr,这样就会导致有两个shared_ptr通过不同的控制块,管理相同的对象。一旦其中一个shared_ptr释放了所管理的对象,那么另一个shared_ptr将会变成非法的。
不太清楚的可以参考文章:【C++】shared_ptr共享型智能指针详解。
如果想要返回this指针,需要如下操作:
class Frame : public std::enable_shared_from_this<Frame> {
public:
std::shared_ptr<Frame> GetThis() {
return shared_from_this();
}
};
至于为什么要这样做,可以参考基类enable_shared_from_this的伪代码:
template<class D>
class enable_shared_from_this {
protected:
constexpr enable_shared_from_this() { }
enable_shared_from_this(enable_shared_from_this const&) { }
enable_shared_from_this& operator=(enable_shared_from_this const&) {
return *this;
}
public:
shared_ptr<T> shared_from_this() { return self_; }
shared_ptr<T const> shared_from_this() const { return self_; }
private:
weak_ptr<D> self_;
friend shared_ptr<D>;
};
template<typename T>
shared_ptr<T>::shared_ptr(T* ptr) {
// ...
// Code that creates control block goes here.
// ...
// NOTE: This if check is pseudo-code. Won't compile. There's a few
// issues not being taken in to account that would make this example
// rather noisy.
if (is_base_of<enable_shared_from_this<T>, T>::value) {
enable_shared_from_this<T>& base = *ptr;
base.self_ = *this;
}
}
这里就是采用CRTP的方法。
Eigen库
为了提升性能,Eigen用了很多模板元编程技术,其中就用到了许多CRTP的编程方法。
同时还使用到了,表达式模板(expression-template)。这也是一种C++模板元编程技术,它在编译时刻构建好一些能够表达某一计算的结构,对于整个计算这些结构仅在需要(惰性计算、延迟计算)的时候才产生相关有效的代码运算。因此,表达式模板允许程序员绕过正常的C++语言计算顺序,从而达到优化计算的目的。
例如:对于解决一个向量加法表达式 v 3 = v 0 + v 1 + v 2 v_3=v_0+v_1+v_2 v3=v0+v1+v2,传统的编程方式存在中间内存的申请与释放、多次循环遍历等空间和时间的低效问题。但是利用奇异递归模板模式(CRTP)、表达式模板(Expression Templates)就可以解决它。
这部分的代码少许复杂,想要了解的可以参考文章:【编程技术】C++ CRTP & Expression Templates。