【C++】奇异递归模板模式CRTP——静态多态

奇异递归模板模式(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通过派生类对基类模板实例化,也可以实现类似动态多态的效果。最鲜明的特点就是:

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

一个简单的例子:

#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


相关阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值