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

1.初见

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

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来获取指向该类的智能指针。

我们来看看具体的代码实现逻辑:

structGood:std::enable_shared_from_this// 注意:继承{std::shared_ptr getptr() {returnshared_from_this();    }};structBad{// 错误写法:用不安全的表达式试图获得 this 的 shared_ptr 对象std::shared_ptr getptr() {returnstd::shared_ptr(this);    }};

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

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

为什么Bad类直接通过this构造shared_ptr会存在问题?

答:因为原本的this指针就是被shared_ptr管理的,通过getprt函数构造的新的智能指针和和原本管理this指针的的shared_ptr并不互相感知。这会导致指向Bad的this指针被二次释放!!!

2.为什么通过继承std::enable_shared_from_this之后就没有上述问题了?

答:这里截取了部分std::enable_shared_from_this的源码并且简化了一下:

templateclassenable_shared_from_this{protected:      enable_shared_from_this(constenable_shared_from_this&)noexcept{ }      ~enable_shared_from_this() { }public:shared_ptr<_Tp>      shared_from_this()      {returnshared_ptr<_Tp>(this->_M_weak_this); }shared_ptr      shared_from_this()const{returnshared_ptr(this->_M_weak_this); }private:mutableweak_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的代码之中我们可以看到。它核心的作用是利用子类的信息来生成代码,我们来具体看看对应的代码实现

这里通过子类的模板信息,在父类之中派生出一个指向自身的weak_ptr。

private:mutableweak_ptr<_Tp>  _M_weak_this;

派生出了可以生成子类的函数shared_from_this:

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

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

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

这种用法很常见,笔者常用的Boost.operators同样也使用了CRTP,通过继承其中的boost::less_than_comparable, 可以很轻松的替代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的使用,利用静态多态的方式。将虚函数的调用转换为函数指针的调用,这个在实际聚合函数的实现过程之中能够大大提高计算的效率。我们来看看具体的代码:

templateclassIAggregateFunctionHelper:publicIAggregateFunction{private:staticvoidaddFree(constIAggregateFunction * that, AggregateDataPtr place,constIColumn ** columns,size_trow_num, Arena * arena){static_cast(*that).add(place, columns, row_num, arena);    }public:AddFuncgetAddressOfAddFunction()constoverride{return&addFree; }

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

classAggregateFunctionCountfinal:publicIAggregateFunctionDataHelper{public:    AggregateFunctionCount(constDataTypes & argument_types_) : IAggregateFunctionDataHelper(argument_types_, {}) {}voidadd(AggregateDataPtr place,constIColumn **,size_t, Arena *)constoverride{        ++data(place).count;    }

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

** The innerloopthat uses thefunctionpointerisbetter thanusingthe virtualfunction. * The reasonisthatinthecaseofvirtual functions GCC5.1.2generates code, *  which, ateachiterationoftheloop, reloads thefunctionaddress (the offset valueinthe virtualfunctiontable)frommemorytothe register. * This gives a performance droponsimple queries around12%. * After the appearanceofbetter compilers, the code can be removed.

2.2: 颠倒继承

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

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

InternalQueueBase类

Doris实现了一个线程安全的Queue结构,它的内部实现了一个Node类。它的next与prev函数就是利用了颠倒继承与reinterpret_cast的强制类型转换,让父类获取了能够返回子类指针的能力,从而让子类再通过继承拥有了对应的能力。

templateclassInternalQueueBase{public:structNode{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 lock(parent_queue->lock_);returnreinterpret_cast(next_node);    }T*prev()const{      boost::lock_guard lock(parent_queue->lock_);returnreinterpret_cast(prev_node);    }private:friendclassInternalQueueBase;Node* next_node;    Node* prev_node;  };

这里Block类通过CRTP的方式继承了InternalQueue::Node, 便自动拥有了成为Queue中节点的能力,能够成为线程安全的Queue的元素了。而Block类的next与prev方法便自动能够返回指向Block的指针了。

classBlock:publicInternalQueue::Node{public:        // A null dtor to pass codestyle check        ~Block() {}

深圳网站建设www.sz886.com

你可能感兴趣的:(C++雾中风景14:CRTP, 模板的黑魔法)