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源碼

相關文章
相關標籤/搜索