CRTP,奇特的遞歸模板模式 (Curiously Recurring Template Pattern) 是 C++ 的一種看起來很怪異的模板編程技巧。
它經過繼承和模板的聯合應用,實現了一種"看似"繼承本身的語法。這種編程的技法,不管是在STL仍是Boost之中都被大量使用。像它的名字同樣,看起來很Curiously。筆者在進行數據庫源碼學習和開發時,發現不管是Clickhouse仍是Doris中也一樣大量使用了這種編程技巧來簡化代碼和提升性能。
接下來,用一杯咖啡的時間,來和你們詳細聊聊這種模板的黑魔法。node
First of All, 咱們先瞅瞅CRTP長啥樣。git
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
這裏只分析下面兩個問題:編程
getprt
函數構造的新的智能指針和和本來管理this指針的的shared_ptr並不互相感知。這會致使指向Bad的this指針被二次釋放!!!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的源碼更爲完全的整明白它的實現。架構
咱們重點來看看,這個CRTP在上文的enable_shared_from_this
之中起到了怎麼樣的做用。從1.1的代碼之中咱們能夠看到。它核心的做用是利用子類的信息來生成代碼,咱們來具體看看對應的代碼實現app
private: mutable weak_ptr<_Tp> _M_weak_this;
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啦,媽媽不再用擔憂我寫很差重載操做符了~~)
在上一節之中,咱們瞭解了CRTP的實現。固然這種「奇技淫巧」並非用來裝逼的。因此本節筆者就結合本身自己的實踐,來描述一下CRTP應該如何在實際的編碼場景之中使用,以及可以解決一些什麼樣的問題。
在Clickhouse之中,大量使用了CRTP來實現靜態多態的形式來減小虛函數的調度開銷。
Clickhouse使用了數據庫之中經典的執行模式Volcano model:
數據以一個個tuple形式在操做符之間傳遞,而因爲操做符之間不斷交互,致使了大量的虛函數調用開銷,影響執行效率。由於虛函數的調用須要經過指針查找虛函數表來進行調用,同時類的對象由於不須要存儲虛函數指針,也會帶來一部分存儲的開銷。而經過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.
說完了Clickhouse,固然得提一嘴自家的Doris。Doris之中應用了CRTP來實現顛倒繼承的目的。
顛倒繼承(Upside Down Inheritance),顧名思義就是經過父類向子類添加功能。由於它的效果與普通繼承父到子的邏輯是相反的。第一節的enable_shared_from_this
就是利用了顛倒繼承來實現所須要的功能的。接下來,咱們來看看Doris的代碼吧:
next
與prev
函數就是利用了顛倒繼承與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類的next
與prev
方法便自動可以返回指向Block的指針了。
class Block : public InternalQueue<Block>::Node { public: // A null dtor to pass codestyle check ~Block() {}
經過CRTP實現顛倒繼承的方式,可以大大減小咱們須要額外編寫的代碼量,簡化咱們的代碼結構和減小coding工做。可是帶來的缺點也很明顯,這種經過模板派生的形式生成的代碼與宏定義通常,相對來講難以理解,不易調試。因此,捨得之間,你們須要本身選擇。
看到這裏,想必你們手裏的咖啡也喝完了哈。本篇介紹了一個模板使用的黑魔法:CRTP。它在高性能數據庫,金融系統領域做爲一種編程技法被大量使用。可是因爲其怪異的語法,坦率來講對初學者並不友好。
管中窺豹,咱們能夠經過CRTP看到C++模板的強大魅力。不管是在代碼簡化,性能提高方面都值得咱們繼續深刻思考學習,也歡迎你們多多討論,指教。