慣用法之 CRTP
你好,我是雨樂!
在之前的文章 << 多態實現 - 虛函數、函數指針以及變體 >> 一文中,提到了多態的幾種實現方式,今天,藉助這篇文章,聊聊多態的另外一種實現方式CRTP
。
概念
CRTP 是Curiously 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