C++雾中风景14:CRTP, 模板的黑魔法

CRTP,奇特的递归模板模式 (Curiously Recurring Template Pattern) 是 C++ 的一种看起来很怪异的模板编程技巧。
它经过继承和模板的联合应用,实现了一种"看似"继承本身的语法。这种编程的技法,不管是在STL仍是Boost之中都被大量使用。像它的名字同样,看起来很Curiously。笔者在进行数据库源码学习和开发时,发现不管是Clickhouse仍是Doris中也一样大量使用了这种编程技巧来简化代码和提升性能。
接下来,用一杯咖啡的时间,来和你们详细聊聊这种模板的黑魔法node

1.初见

First of All, 咱们先瞅瞅CRTP长啥样。git

1.1:std::enable_shared_from_this

C++11 引入了一个典型的CRTP的类:std::enable_shared_from_this
当咱们有类须要被智能指针share_ptr管理,且须要经过类的成员函数里须要把当前类对象包装为智能指针传递出一个指向自身的share_ptr时。在这种状况下类就须要经过继承enable_shared_from_this,经过父类的成员函数shared_from_this来获取指向该类的智能指针。github

咱们来看看具体的代码实现逻辑:数据库

struct Good: std::enable_shared_from_this<Good> // 注意:继承
{
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};

struct Bad
{
    // 错误写法:用不安全的表达式试图得到 this 的 shared_ptr 对象
    std::shared_ptr<Bad> getptr() {
        return std::shared_ptr<Bad>(this);
    }
};

这里咱们能够看到,Good类继承了std::enable_shared_from_this,而且本身是做为模板参数传递给父类的。这就给让代码看起来有些"唬人",看起来像是继承本身同样。但其实呢?这里只是用到了模板派生,让父类可以在编译器感知到子类的模板存在,两者不是真正意义上的继承关系。apache

这里只分析下面两个问题:编程

    1. 为何Bad类直接经过this构造shared_ptr会存在问题?
      答:由于本来的this指针就是被shared_ptr管理的,经过getprt函数构造的新的智能指针和和本来管理this指针的的shared_ptr并不互相感知。这会致使指向Bad的this指针被二次释放!!!
  • 2.为何经过继承std::enable_shared_from_this以后就没有上述问题了?
    答:这里截取了部分std::enable_shared_from_this的源码而且简化了一下:
template<typename _Tp>
    class enable_shared_from_this
    {
    protected:

      enable_shared_from_this(const enable_shared_from_this&) noexcept { }
      ~enable_shared_from_this() { }

    public:
      shared_ptr<_Tp>
      shared_from_this()
      { return shared_ptr<_Tp>(this->_M_weak_this); }

      shared_ptr<const _Tp>
      shared_from_this() const
      { return shared_ptr<const _Tp>(this->_M_weak_this); }

    private:
      mutable weak_ptr<_Tp>  _M_weak_this;
    };

std::enable_shared_from_this的实现因为有些复杂,受限于篇幅。笔者就不展开来分析它具体是怎么样实现的了。它的可以规避上述问题的缘由以下:安全

  • 经过自身维护了一个std::weak_ptr让全部从该对象派生的shared_ptr都经过了std::weak_ptr构造派生。
  • std::shared_ptr的构造函数判断出对象是std::enable_shared_from_this的之类以后也会一样经过对象自己的std::weak_ptr构造派生。这个这样引用计数是互通的,也就不会存在上述double delete的问题了。

enable_shared_from_this的实现逻辑不是本篇的重点,感兴趣的朋友能够自行看看STL的源码更为完全的整明白它的实现。架构

1.2:CRTP的使用

咱们重点来看看,这个CRTP在上文的enable_shared_from_this之中起到了怎么样的做用。从1.1的代码之中咱们能够看到。它核心的做用是利用子类的信息来生成代码,咱们来具体看看对应的代码实现app

  1. 这里经过子类的模板信息,在父类之中派生出一个指向自身的weak_ptr。
private:
      mutable weak_ptr<_Tp>  _M_weak_this;
  1. 派生出了能够生成子类的函数shared_from_this:
shared_ptr<_Tp>
      shared_from_this()
{ return shared_ptr<_Tp>(this->_M_weak_this); }

经过这两个核心的派生逻辑,大致上就完成了enable_shared_from_this的骨架构建了。less

因此,其实CRTP只不过是表面上看起来有些"唬人"。它的核心做用就是只有一条:是利用子类的信息来生成代码。

这种用法很常见,笔者经常使用的Boost.operators一样也使用了CRTP,经过继承其中的boost::less_than_comparable<class>, 能够很轻松的替代std::rel_ops,来代替咱们生成比较操做符的代码。(std::rel_ops这玩意太他喵难用了,我历来都是用boost 替代的。固然,C++20引入了<=>Spaceship Operator,咱们也能够抛弃Boost啦,妈妈不再用担忧我写很差重载操做符了~~)

2.How To Use

在上一节之中,咱们了解了CRTP的实现。固然这种“奇技淫巧”并非用来装逼的。因此本节笔者就结合本身自己的实践,来描述一下CRTP应该如何在实际的编码场景之中使用,以及可以解决一些什么样的问题。

2.1: 静态多态

在Clickhouse之中,大量使用了CRTP来实现静态多态的形式来减小虚函数的调度开销。

Clickhouse使用了数据库之中经典的执行模式Volcano model:
数据以一个个tuple形式在操做符之间传递,而因为操做符之间不断交互,致使了大量的虚函数调用开销,影响执行效率。由于虚函数的调用须要经过指针查找虚函数表来进行调用,同时类的对象由于不须要存储虚函数指针,也会带来一部分存储的开销。而经过CRTP,偏偏就能经过静态多态的方式,规避上述问题。

  • IAggregateFunctionHelper接口
    Clickhouse的聚合函数继承了IAggregateFunctionHelper接口。它就是一个典型的CRTP的使用,利用静态多态的方式。将虚函数的调用转换为函数指针的调用,这个在实际聚合函数的实现过程之中可以大大提升计算的效率。咱们来看看具体的代码:
template <typename Derived>
class IAggregateFunctionHelper : public IAggregateFunction
{
private:
    static void addFree(const IAggregateFunction * that, AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena * arena)
    {
        static_cast<const Derived &>(*that).add(place, columns, row_num, arena);
    }

public:
    AddFunc getAddressOfAddFunction() const override { return &addFree; }

咱们选取一个聚合函数AggregateFunctionCount来看,它继承了IAggregateFunctionHelper。而经过getAddressOfAddFunction就能够经过addFree的强制类型转换,直接得到子类的函数指针.(这个过程在编译期间就能够完成,因此称之为静态多态。) 经过这种CRTP的巧妙方式下降了上面提到的虚函数开销。

class AggregateFunctionCount final : public IAggregateFunctionDataHelper<AggregateFunctionCountData, AggregateFunctionCount>
{
public:
    AggregateFunctionCount(const DataTypes & argument_types_) : IAggregateFunctionDataHelper(argument_types_, {}) {}

    void add(AggregateDataPtr place, const IColumn **, size_t, Arena *) const override
    {
        ++data(place).count;
    }

在Clickhouse的代码注释之中提到,经过CRTP的方式,可以有12%的性能提高。可见这种静态多态的方式对于OLAP的系统的性能的确是有显著的提高的。

** The inner loop that uses the function pointer is better than using the virtual function.
 * The reason is that in the case of virtual functions GCC 5.1.2 generates code,
 *  which, at each iteration of the loop, reloads the function address (the offset value in the virtual function table) from memory to the register.
 * This gives a performance drop on simple queries around 12%.
 * After the appearance of better compilers, the code can be removed.
2.2: 颠倒继承

说完了Clickhouse,固然得提一嘴自家的Doris。Doris之中应用了CRTP来实现颠倒继承的目的。

颠倒继承(Upside Down Inheritance),顾名思义就是经过父类向子类添加功能。由于它的效果与普通继承父到子的逻辑是相反的。第一节的enable_shared_from_this就是利用了颠倒继承来实现所须要的功能的。接下来,咱们来看看Doris的代码吧:

  • InternalQueueBase类
    Doris实现了一个线程安全的Queue结构,它的内部实现了一个Node类。它的nextprev函数就是利用了颠倒继承与reinterpret_cast<T*>的强制类型转换,让父类获取了可以返回子类指针的能力,从而让子类再经过继承拥有了对应的能力。
template <typename LockType, typename T>
class InternalQueueBase {
 public:
  struct Node {
   public:
    Node() : parent_queue(NULL), next_node(NULL), prev_node(NULL) {}
    virtual ~Node() {}

    /// Returns the Next/Prev node or NULL if this is the end/front.
    T* next() const {
      boost::lock_guard<LockType> lock(parent_queue->lock_);
      return reinterpret_cast<T*>(next_node);
    }
    T* prev() const {
      boost::lock_guard<LockType> lock(parent_queue->lock_);
      return reinterpret_cast<T*>(prev_node);
    }

   private:
    friend class InternalQueueBase<LockType, T>;

    Node* next_node;
    Node* prev_node;
  };

这里Block类经过CRTP的方式继承了InternalQueue<Block>::Node, 便自动拥有了成为Queue中节点的能力,可以成为线程安全的Queue的元素了。而Block类的nextprev 方法便自动可以返回指向Block的指针了。

class Block : public InternalQueue<Block>::Node {
    public:
        // A null dtor to pass codestyle check
        ~Block() {}

经过CRTP实现颠倒继承的方式,可以大大减小咱们须要额外编写的代码量,简化咱们的代码结构和减小coding工做。可是带来的缺点也很明显,这种经过模板派生的形式生成的代码与宏定义通常,相对来讲难以理解,不易调试。因此,舍得之间,你们须要本身选择。

3.小结

看到这里,想必你们手里的咖啡也喝完了哈。本篇介绍了一个模板使用的黑魔法:CRTP。它在高性能数据库,金融系统领域做为一种编程技法被大量使用。可是因为其怪异的语法,坦率来讲对初学者并不友好。
管中窥豹,咱们能够经过CRTP看到C++模板的强大魅力。不管是在代码简化,性能提高方面都值得咱们继续深刻思考学习,也欢迎你们多多讨论,指教。

4.参考资料

维基百科:CRTP
ClickHouse源码
Doris源码

相关文章
相关标签/搜索