版权归原作者所有,如有侵权,请联系我们

[科普中国]-奇异递归模板模式

科学百科
原创
科学百科为用户提供权威科普内容,打造知识科普阵地
收藏

奇异递归模板模式是C++模板编程时的一种惯用法(idiom):把派生类作为基类的模板参数。更一般地被称作F-bound polymorphism,是一类F 界量化,相关介绍可以参考 wiki 奇异递归模板模式。

简介一般形式templateclass Base{ // methods within Base can use template to access members of Derived};class Derived : public Base{ // ...};CRTP的特点继承自模板类;

2. 使用派生类作为模板参数特化基类;

CRTP基本范式CRTP如下的代码样式:

template class Base{ ...}; // use the derived class itself as a template parameter of the base classclass Derived : public Base{ ...};这样做的目的是在基类中使用派生类,从基类的角度来看,派生类其实也是基类,通过向下转换[downcast],因此,基类可以通过static_cast把其转换到派生类,从而使用派生类的成员,形式如下:

template class Base{public: void doWhat() { T& derived = static_cast(*this); // use derived... }};注意 这里不使用dynamic_cast,因为dynamic_cast一般是为了确保在运行期(run-time)向上向下转换的正确性。CRTP的设计是:派生类就是基类的模板参数,因此static_cast足矣。1

易错点当两个类继承自同一个CRTP base类时,如下代码所示,会出现错误(Derived2派生的基类模板参数不是Derived2)。

class Derived1 : public Base{ ...};class Derived2 : public Base // bug in this line of code{ ...};为了防止种错误的出现,可以写成如下的代码形式:

template class Base{public: // ...private:// import Base(){}; friend T;}; 通过代码可以看出来,基类中添加一个私有构造函数,并且模板参数T是Base的友元。这样做可行是因为,派生类需要调用基类的构造函数(编译器会默认调用的),由于Base的构造函数是私有的(private),除了友元没有其他类可以访问的,而且基类独立的友元是其实例化模板参数。

派生类会隐藏和基类同名的方法,如下代码所示:

template class Base{public: void do();};class Derived : public Base{public: void do(); // oops this hides the doSomething methods from the base class !}出现这个情况的缘由是,以作用域为基础的“名称遮掩规则”,从名称查找的观点来看,如果实现了Derived::do(), 则Base::do不再被Derived继承。2

静态多态Andrei Alexandrescu在Modern C++ Design中称之为静态多态(static polymorphism)。

template struct Base{ void interface() { // ... static_cast(this)->implementation(); // ... } static void static_func() { // ... T::static_sub_func(); // ... }};struct Derived : Base{ void implementation(); static void static_sub_func();};基类模板利用了其成员函数体(即成员函数的实现)将不被实例化直至声明很久之后(实际上只有被调用的模板类的成员函数才会被实例化);并利用了派生类的成员,这是通过{{ilh|lang={{langname|Type conversion}}|lang-code=Type conversion|1=类型转化|2=类型转化|d=|nocat=}}。

在上例中,Base::interface(),虽然是在struct Derived之前就被声明了,但未被编译器实例化直至它被实际调用,这发生于Derived声明之后,此时Derived::implementation()的声明是已知的。

这种技术获得了类似于虚函数的效果,并避免了动态多态的代价。也有人把CRTP称为“模拟的动态绑定”。

这种模式广泛用于WindowsATL与WTL库,以及Boost.Iterator,Boost.Python或者Boost.Serialization等库中。3

考虑一个基类,没有虚函数,则它的成员函数能够调用的其它成员函数,只能是属于该基类自身。当从这个基类派生其它类时,派生类继承了所有未被覆盖(overridden)的基类的数据成员与成员函数。如果派生类调用了一个被继承的基类的函数,而该函数又调用了其它成员函数,这些成员函数不可能是派生类中的派生或者覆盖的成员函数。也就是说,基类中是看不到派生类的。但是,基类如果使用了CRTP,则在编译时派生类的覆盖的函数可被选中调用。这效果相当于编译时模拟了虚函数调用但避免了虚函数的尺寸与调用开销(VTBL结构与方法查找、多继承机制)等代价。但CRTP的缺点是不能在运行时做出动态绑定。

不通过虚函数机制,基类访问派生类的私有或保护成员,需要把基类声明为派生类的友元(friend)。如果一个类有多个基类都出现这种需求,声明多个基类都是友元会很麻烦。一种解决技巧是在派生类之上再派生一个accessor类,显然accessor类有权访问派生类的保护函数;如果基类有权访问accessor类,就可以间接调用派生类的保护成员了。这种方法被boost的多个库使用,如:Boost.Python中的def_visitor_access和Boost.Iterator的iterator_core_access。原理示例代码如下:

template class Base{ private: struct accessor : DerivedT { // accessor类没有数据成员,只有一些静态成员函数 static int foo(DerivedT& derived) { int (DerivedT::*fn)() = &DeriveT::do_foo; //获取DerivedT::do_foo的成员函数指针 return (derived.*fn)(); // 通过成员函数指针的函数调用 } }; // accessor类仅是Base类的成员类型,而没有实例化为Base类的数据成员。 public: DerivedT& derived() // 该成员函数返回派生类的实例的引用 { return static_cast(*this); } int foo() { // 该函数具体实现了业务功能 return accessor::foo( this->derived()); }}; struct Derived : Base // 派生类不需要任何特别的友元声明 protected: int do_foo() { // ... 具体实现 return 1; }};例子对象计数统计一个类的实例对象创建与析构的数据。This can be easily solved using CRTP:

template struct counter{ static int objects_created; static int objects_alive; counter() { ++objects_created; ++objects_alive; } counter(const counter&) { ++objects_created; ++objects_alive; }protected: ~counter() // objects should never be removed through pointers of this type { --objects_alive; }};template int counter::objects_created( 0 );template int counter::objects_alive( 0 );class X : counter{ // ...};class Y : counter{ // ...};多态复制构造当使用多态时,常需要基于基类指针创建对象的一份拷贝。常见办法是增加clone虚函数在每一个派生类中。使用CRTP,可以避免在派生类中增加这样的虚函数。

// Base class has a pure virtual function for cloningclass Shape {public: virtual ~Shape() {} virtual Shape *clone() const = 0;};// This CRTP class implements clone() for Derivedtemplate class Shape_CRTP : public Shape {public: virtual Shape *clone() const { return new Derived(static_cast(*this)); }};// Nice macro which ensures correct CRTP usage#define Derive_Shape_CRTP(Type) class Type: public Shape_CRTP// Every derived class inherits from Shape_CRTP instead of ShapeDerive_Shape_CRTP(Square) {};Derive_Shape_CRTP(Circle) {};不可派生的类template class MakeFinally{ private: MakeFinally(){};//只有MakeFinally的友类才可以构造MakeFinally ~MakeFinally(){}; friend T;};class MyClass:public virtual MakeFinally{};//MyClass是不可派生类//由于虚继承,所以D要直接负责构造MakeFinally类,从而导致编译报错,所以D作为派生类是不合法的。class D: public MyClass{};//另外,如果D类没有实例化对象,即没有被使用,实际上D类是被编译器忽略掉而不报错int main(){MyClass var1;// D var2; //这一行编译将导致错误,因为D类的默认构造函数不合法}std::enable_shared_from_thisclass mySharedClass:public std::enable_shared_from_this{public: // ...};int main(){ std::vector(std::shared_ptr) spv; spv.push_back(new mySharedClass()); std::shared_ptr p(new mySharedClass()); mySharedClass &c=*p; spv.emplace_back(c.shared_from_this());}CRTP特点总结CRTP是一种静态多态(static polymorphism/Static binding/Compile-Time binding)与其对应的是动态多态(dynamic polymorphism/Dynamic binding/Run-Time binding)。静态多态与和动态的区别是:多态是动态绑定(运行时绑定 run-time binding),CRTP是静态绑定(编译时绑定 compile-time binding)。其中,动态多态在实现多态时,需要重写虚函数,这种运行时绑定的操作往往需要查找虚表等,效率低。另,template的核心技术在于编译期多态机制,与运行期多态(runtime polymorphism)相比,这种动态机制提供想编译期多态性,给了程序运行期无可比拟的效率优势。因此,如果想在编译期确定通过基类来得到派生类的行为,CRTP便是一种绝佳的选择。

AD(automatic differentiation,自动微分),相关autodiff工具中也有相当一部分这种库在使用CRTP技术,这是由于在数值计算中,对于不同的模型会使用不同的方法,一般使用继承提供统一接口,但又希望不损失效率,因此,此时便可利用CRTP了,子类的operator(expression)实现将覆盖基类的operator实现,并可以编译期静态绑定至子类的方法,AD自动求导效率堪比手动写出相关程序(所谓的 Hand coded)。4

本词条内容贡献者为:

李岳阳 - 副教授 - 江南大学