慣用法之 CRTP

你好,我是雨樂!

在之前的文章 << 多態實現 - 虛函數、函數指針以及變體 >> 一文中,提到了多態的幾種實現方式,今天,藉助這篇文章,聊聊多態的另外一種實現方式CRTP

概念

CRTPCuriously Recurring Template Pattern的縮寫,中文譯爲奇異的遞歸模板模式,是由James O. Coplien在其 1995 年的論文中首次提出。

wikipedia給出了 CRTP 的一般形式,如下:

 // The Curiously Recurring Template Pattern (CRTP)
template <class T>
class Base
{
    // methods within Base can use template to access members of Derived
};
class Derived : public Base<Derived>
{
    // ...
};

看到這種定義方式,你一定感到很奇怪,其實,這種實現方式稱爲繼承自模板。有一定編程經驗的人,如果對於智能指針比較熟悉,那麼已經無意中接觸過這種技術,如下例子:

class Test: public std::enable_shared_from_this<Test> {
  // ...
};

如果與 wikipedia 給出的 CRTP 做對比,在這個示例中,Test 對應於 CRTP 形式中的 Derive 的,而 std::enable_shared_from_this() 則對應於 Base 類。

那麼,這樣做的好處或者目的是什麼呢?

其實,這樣做的目的其實很明確,從基類對象的角度來看,派生類對象其實就是本身,這樣的話只需要使用類型轉換就可以把基類轉化成派生類,從而實現基類對象對派生對象的訪問。

爲了便於更加清晰的理解,完整舉例如下:

template <typename T>
class Base {
public:
    void interface() {
        static_cast<T*>(this)->imp();
    };
};

class Derived : public Base<Derived> {
public:
    void imp() {
        std::cout<< "in Derived::imp" << std::endl;  
    }
};

int main() {
  Base<Derived> b;
  d.interface();
  
  return 0;
}

在上述例子中,我們發現在 Base 類的 interface 接口中,使用static_cast進行類型轉換,從而調用派生類的成員函數。可能會有人感到好奇,爲什麼不用dynamic_cast進行類型轉換呢?主要是因爲 dynamic_cast 應用於運行時,而模板是在編譯器就進行了實例化

編譯運行,輸出結果如下:

in Derived::imp

從上面的輸出結果可以看出,即使我們沒有聲明virtual函數,也實現了多態的功能。

截止到此,我們對 CRTP 有了一個初步的認識,總結起來,其有以下兩個特點:

顛倒繼承

仍然使用上一節中的例子,如下:

template <typename T>
class Base {
public:
    void interface() {
        static_cast<T*>(this)->implementation();
    };
};

class Derived : public Base<Derived> {
public:
    void imp() {
        std::cout << "in Derived::imp" << std::endl;  
    }
};

int main() {
  Base<Derived> b;
  d.interface();
  
  return 0;
}

在這個例子中,派生類 Derived 中定義了一個成員函數imp(),而該函數在基類 Base 中是沒有聲明的,所以,我們可以理解爲對於 CRTP,在基類中調用派生類的成員函數,擴展了基類的功能。而對於普通繼承,則是派生類中調用基類的成員函數,擴展了派生類的功能,這就是我們所說的顛倒繼承

使用場景

俗話說,存在即合理。既然有 CRTP,那麼其必然有自己存在的道理。那麼 CRTP 都用在什麼場景呢?

靜態多態

其實,在前面的例子中,已經大致瞭解了使用 crtp 技術來實現多態功能,該種實現方式爲靜態多態,是在編譯期實現的。

下面通過一個具體的例子來理解靜態多態。

#include <iostream>

#include <iostream>

template <typename T>
class Base{
 public:
  void interface(){
    static_cast<T*>(this)->imp();
  }
  void imp(){
    std::cout << "in Base::imp" << std::endl;
  }
};

class Derived1 : public Base<Derived1> {
 public:
  void imp(){
    std::cout << "in Derived1::imp" << std::endl;
  }
};

class Derived2 : public Base<Derived2> {
 public:
  void imp(){
    std::cout << "in Derived2::imp" << std::endl;
  }
};

class Derived3 : public Base<Derived3>{};

template <typename T>
void fun(T& base){
    base.interface();
}


int main(){
  Derived1 d1;
  Derived2 d2;
  Derived3 d3;

  fun(d1);
  fun(d2);
  fun(d3);

  return 0;
}

在上述代碼中,定義了一個函數fun(),在其函數體內調用interface()函數。如果類型爲 Derived1 和 Derived2,則會調用這倆類型對應的 imp() 函數。而對於 Derived3,因爲其類內沒有實現 imp() 函數,所以調用的是 Base 類即基類的 imp 函數。

編譯運行之後,輸出如下:

in Derived1::imp
in Derived2::imp
in Base::imp

從上述輸出可以看出,即使不使用 virtual,也實現了多態功能,其二者的區別是:virtual 是運行時多態,而 CRTP 則是在編譯期就對模板進行了實例化,所以屬於靜態多態。

代碼複用

假如,現在需要實現一個功能,根據對象的具體類型,輸出其類型名稱。

class Base {
 public:
  void PrintType() {
    std::cout << typeid(*this).name() << std::endl;
  }
};

class Derived : public Base {};
class Derived1 : public Base {};

void PrintType(const Base& base) {
  base.PrintType();
}

對於此種需求,首先想到的是使用 virtual 的多態功能實現,代碼也很好寫,如下:

#include <iostream>
#include <typeinfo>

class Base {
 public:
  virtual void PrintType() const {
    std::cout << typeid(*this).name() << std::endl;
  }
};

class Derived : public Base {
 public:
  virtual void PrintType() const {
    std::cout << typeid(*this).name() << std::endl;
  }
};
class Derived1 : public Base {
 public:
  virtual void PrintType() const {
    std::cout << typeid(*this).name() << std::endl;
  }
};

void PrintType(const Base& base) {
  base.PrintType();
}

int main() {
  Derived d;
  Derived1 d1;

  PrintType(d);
  PrintType(d1);

  return 0;
}

輸出如下:

7Derived
8Derived1

ps: 需要注意的是,在上面的輸出類型中,前面都有一個數字,這個數字是類名的長度

而如果使用 CRTP,則實現如下:

#include <iostream>
#include <typeinfo>

template<typename T>
class Base {
 public:
  void PrintType() {
    T &t = static_cast<T&>(*this);
    std::cout << typeid(t).name() << std::endl;
  }
};

class Derived : public Base<Derived> {};
class Derived1 : public Base<Derived1> {};

template<typename T>
void PrintType(T base) {
  base.PrintType();
}

int main() {
  Derived d;
  Derived1 d1;

  PrintType(d);
  PrintType(d1);

  return 0;
}

函數輸出如下:

7Derived
8Derived1

通過上述輸出可以看出,即使在 Derived 和 Derived1 類中沒有定義 PrintType() 函數,也實現了與 virtual 函數一樣的效果,所以使用 CRTP 的另外一個好處是避免冗餘代碼

性能對比

既然 crtp 也能實現多態,那麼就有必要跟傳統的運行時多態實現方式 virtual 來做下對比。

virtual.cc:

#include <iostream>
#include <typeinfo>
#include <sys/time.h>

class Base {
 public:
  virtual void PrintType() const {
  }
};

class Derived : public Base {
 public:
  virtual void PrintType() const {
  }
};
class Derived1 : public Base {
 public:
  virtual void PrintType() const {
  }
};

void PrintType(const Base& base) {
  base.PrintType();
}

int main() {
  Derived d;
  Derived1 d1;

  struct timeval start;
  struct timeval end;
  gettimeofday(&start_, NULL);
  for (int i = 0; i < 1000000; ++i) {
    PrintType(d);
    PrintType(d1);
  }
  gettimeofday(&end, nullptr);
  double cost = 1000000 * (end.tv_sec - start.tv_sec) +
                  end.tv_usec - start.tv_usec;

  std::cout << "virtual time cost " << cost << std::endl;
  return 0;
}

crtp.cc:

#include <iostream>
#include <typeinfo>
#include <sys/time.h>

template<typename T>
class Base {
 public:
  void PrintType() const {
    T &t = static_cast<T&>(*this);
    t.PrintType();
  }
};

class Derived : public Base<Derived> {
 public:
  void PrintType() const {}
};
class Derived1 : public Base<Derived1> {
 public:
  void PrintType() const {}
};

template<typename T>
void PrintType(T base) {
  base.PrintType();
}

int main() {
  Derived d;
  Derived1 d1;

  struct timeval start;
  struct timeval end;
  gettimeofday(&start, nullptr);
  for (int i = 0; i < 1000000; ++i) {
    PrintType(d);
    PrintType(d1);
  }
  gettimeofday(&end, NULL);
  double cost = 1000000 * (end.tv_sec - start.tv_sec) +
                  end.tv_usec - start.tv_usec;

  std::cout << "crtp time cost " << cost << std::endl;
  return 0;
}

對上述兩段代碼分別編譯運行之後,輸出如下:

virtual time cost 22757
crtp time cost 7871

從上述輸出可以看出,crtp 的耗時是 virtual 實現方式的 1/3。

侷限性

既然 CRTP 能實現多態性,且其性能優於 virtual,那麼 virtual 還有沒有存在的必要麼?

雖然 CRTP 最終還是調用派生類中的成員函數。但是,問題在於 Base 類實際上是一個模板類,而不是一個實際的類。因此,如果存在名爲 Derived 和 Derived1 的派生類,則基類模板初始化將具有不同的類型。這是因爲,Base 類將進行不同類型的特化,代碼如下:

class Derived : public Base<Derived> {
 void imp(){
    std::cout << "in Derived::imp" << std::endl;
  }
};

class Derived1 : public Base<Derived1> {
 void imp(){
    std::cout << "in Derived1::imp" << std::endl;
  }
};

如果創建 Base 類模板的指針,則意味着存在兩種類型的 Base 指針,即:

// CRTP
Base<Derived> *b = new Derived;
Base<Derived> *b1 = new Derived1;

顯然,這與我們虛函數的方式不同。因爲,動態多態性只給出了一種 Base 指針。但是現在,每個派生類都可以使用不同的指針類型。

// virtual
Base *v1 = new Derived;
Base *v2 = new Derived1;

正是因爲基於 CRTP 方式的指針具有不同的類型,所以不能將CRTP基類指針存儲在容器中,下面的代碼將編譯失敗:

int main() {
  Base<Derived> *d = new Derived;
  Base<Derived> *d1 = new Derived1;

  auto vec = {d, d1};

  return 0;
}

編譯器輸出如下:

test.cc: In function ‘int main()’:
test.cc:33: error: cannot convert ‘Derived1*’ to ‘Base<Derived>*’ in initialization
test.cc:35: error: ISO C++ forbids declaration of ‘vec’ with no type
test.cc:35: error: scalar object ‘vec’ requires one element in initializer

正是因爲其侷限性,所以 CRTP 是一種特殊類型的多態性,在少數情況下可以替代動態多態性的需要。

結語

通過 CRTP 技術,在某種程度上也可以實現多態功能,但其也僅限定於使用場景,正如侷限性一節中所提到的,CRTP 是一種特殊類型的多態性,在少數情況下可以替代動態多態性的需要;另外,使用 CRTP 技術,代碼可讀性降低、模板實例化之後的代碼膨脹以及無法動態綁定 (在編譯期決實例化),因此,我們可以根據使用場景,來靈活選擇 CRTP 或者 virtual 來達到多態目的。

好了,今天的文章就到這,我們下期見!

如果對本文有疑問可以加筆者微信直接交流,筆者也建了 C/C++ 相關的技術羣,有興趣的可以聯繫筆者加羣。

你好,我是雨樂,從業十二年有餘,歷經過傳統行業網絡研發、互聯網推薦引擎研發,目前在廣告行業從業 8 年。目前任職某互聯網公司高級技術專家一職,負責廣告引擎的架構和研發。

本公衆號專注於架構、技術、線上 bug 分析等乾貨,歡迎關注。

高性能架構探索 畢業於中國科學技術大學,現任某互聯網公司高級技術專家一職。專注於分享乾貨,硬貨,歡迎關注😄

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/giSDMWLO0d7t3fX-ZC7eVg