奇异递归模板模式( Curiously Recurring Template Pattern,CRTP)1

1.CRTP介绍

奇异递归模板模式(curiously recurring template pattern,CRTP)是C++模板编程时的一种惯用法(idiom):把派生类作为基类的模板参数。更一般地被称作F-bound polymorphism,是一类F 界量化,相关介绍可以参考 wiki 奇异递归模板模式。curiously recurring template pattern,CRTP的来源,请参考这篇C++ Report James Coplien in 1995。

1.1 CRTP的特点

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

1.2 CRTP基本范式

CRTP如下的代码样式:

template <typename T>
class Base
{
    ...
};
 // use the derived class itself as a template parameter of the base class
class Derived : public Base
{
    ...
};

这样做的目的是在基类中使用派生类,从基类的角度来看,派生类其实也是基类,通过向下转换[downcast],因此,基类可以通过static_cast把其转换到派生类,从而使用派生类的成员,形式如下:

template <typename T>
class Base
{
public:
    void doWhat()
    {
        T& derived = static_cast(*this);
        // use derived...
    }
};

注意 这里不使用dynamic_cast,因为dynamic_cast一般是为了确保在运行期(run-time)向上向下转换的正确性。CRTP的设计是:派生类就是基类的模板参数,因此static_cast足矣。另,关于cpp几种转换的讨论参见这个When should static_cast, dynamic_cast, const_cast and reinterpret_cast be used?

1.3 易错点

  1. 当两个类继承自同一个CRTP base类时,如下代码所示,会出现错误(Derived2派生的基类模板参数不是Derived2)。
class Derived1 : public Base
{
    ...
};
class Derived2 : public Base // bug in this line of code
{
    ...
};

为了防止种错误的出现,可以写成如下的代码形式:

template <typename T>
class Base
{
public:
    // ...
private:// import 
    Base(){};
    friend T;
};

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

  1. 派生类会隐藏和基类同名的方法,如下代码所示:
template <typename T>
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继承(哎呀,因为被继承类的实现方法遮掩了,即基类的 do方法被隐藏了),详情请见 Effective C++ Item 33。

2. CRTP使用demo

CRTP有几种使用方法,下面介绍CRTP两种常用方法:1. Adding functionality(添加功能,通过继承基类(CRTP)来扩展派生类的接口); 2. Static Interfaces(利用静态多态,编译期多态),见相关问题讨论 CRTP to avoid dynamic polymorphism,来段实例代码先:

//  The Curiously Recurring Template Pattern (CRTP)

#include 
#include 
#include 
#include 
using namespace std;

template <class T>
struct Expression
{
    const T& cast() const {
        return static_cast<const T&>(*this);
    }

    T& cast() {
        return static_cast(*this);
    }
    // expands derived class interface by inheriting from the base class
    void set_absolute_value() {
        cast().set_value(abs(cast().get_value()));
    }

    double value() const {
        return cast().get_value();
    }

    double result() const {
        return cast().get_result();
    }

    void calc() {
        cast().sub_calc();
    }

    Expression& operator=(const Expression&) = delete;

private:
    Expression(){}
    friend T;
};

struct Square : public Expression
{
    Square(double val = 0, double res = 0):val_(val),result_(res){}
    void set_value(const double& val) {
        val_ = val;
    }
    double get_value() const {
        return val_;
    }

    double get_result() const {
        return result_;
    }

    void sub_calc() {
        result_ = val_*val_;
    }
private:
    double val_;
    double result_;
};

struct Sqrt : public Expression
{
    Sqrt(double val = 0, double res = 0) :val_(val), result_(res) {}
    void set_value(const double& val) {
        val_ = val;
    }
    double get_value() const {
        return val_;
    }

    double get_result() const {
        return result_;
    }

    void sub_calc() {
        assert(val_>=0);
        result_ = sqrt(val_);
    }
private:
    double val_;
    double result_;
};

// Static interfaces
template<class T> // T is deduced at compile-time
void OP(Expression &sq, const string& op) {
    // will do static dispatch
    cout << "Berfore OP this expression's value is:" << sq.value() << "\n";
    sq.calc();
    cout << "After OP "<< op <<\
        ",  this expression's result is:" << sq.result() << "\n";
}

int main() {

Sqrt sqr(-2);
cout << "this expression's value is:" << sqr.value() << "\n";
// Adding functionality
sqr.set_absolute_value();
cout << "After set absolute,  this expression's value is:" << sqr.value() << "\n";
OP(sqr, "Sqrt");
cout << "---------------------------\n";
Square sq(-2);
OP(sq, "Square");

return 0;
}

执行结果:
CRTP test

分析:通过上面的程序我们可以看到,继承类 Square,Sqrt 的成员通过继承基类Expression来扩展自己的接口,如基类的set_absolute_value函数,因此,这就是所谓的Adding functionality,从这个角度来看,基类不是接口,继承类不是实现,然而这里恰恰相反,基类使用继承类的方法(get_value and set_value),就这一点而言,继承类给基类提供接口,这和传统的继承有点不同。关于Static interfaces,其实就是一种静态多态(static polymorphism),我们利用相同的接口: void OP(Expression &sq, const string& op)分别调用不同的派生类Square,Sqrt,表现出不同的行为,这些多态调用在编译期就确定了,这样可以避免动态多态虚函数调用的效率消耗。

3. 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)相比,这种动态机制提供想编译期多态性,给了程序运行期无可比拟的效率优势,关于编译期编译期多态和运行期多态效率对比一篇不错的文章,参见The cost of dynamic (virtual calls) vs. static (CRTP) dispatch in C++(这个链接有可能被墙,请)。因此,如果想在编译期确定通过基类来得到派生类的行为,CRTP便是一种绝佳的选择。

最近在学习AD(automatic differentiation,自动微分),相关autodiff工具点这个链接。发现还是有相当一部分这种库在使用CRTP技术,这是由于在数值计算中,对于不同的模型会使用不同的方法,一般使用继承提供统一接口,但又希望不损失效率,因此,此时便可利用CRTP了,子类的operator(expression..)实现将覆盖基类的operator实现,并可以编译期静态绑定至子类的方法,AD自动求导效率堪比手动写出相关程序(所谓的 Hand coded),有机会的话会写一篇AD方便的文章。其实很多C++工程都用到CRTP技术了,比如std::enable_shared_from_this 大法。

参考:

  1. wiki 奇异递归模板模式
  2. Modern C++ design
  3. The Curiously Recurring Template Pattern

你可能感兴趣的:(c/c++/cpp11,静态多态,动态多态,CRTP,C++,奇异递归模板模式)