C++奇異遞歸模板(CRTP)的使用小結(jié)
在 C++ 里,有很多反直覺(jué)的設(shè)計(jì)模式,CRTP(Curiously Recurring Template Pattern,奇異遞歸模板模式)便是其中之一。它以“派生類(lèi)作為基類(lèi)模板參數(shù)”這一奇特語(yǔ)法為核心,實(shí)現(xiàn)了編譯期多態(tài)、零開(kāi)銷(xiāo)代碼復(fù)用,是高性能 C++ 開(kāi)發(fā)、模板元編程(TMP)以及各類(lèi)開(kāi)源框架(如 STL、Boost、Qt)中的核心技術(shù)。
什么是 CRTP?
CRTP 的核心語(yǔ)法非常簡(jiǎn)潔,核心是“派生類(lèi)繼承一個(gè)以自身為模板參數(shù)的基類(lèi)”,這也是“奇異遞歸”名字的由來(lái)——語(yǔ)法上看似形成了遞歸,但編譯期展開(kāi)后并不會(huì)產(chǎn)生執(zhí)行期遞歸,更不會(huì)有棧溢出風(fēng)險(xiǎn)。
最簡(jiǎn)示例代碼如下:
#include <iostream>
using namespace std;
// 1. CRTP 基類(lèi):模板類(lèi),參數(shù)為派生類(lèi)類(lèi)型
template <typename Derived>
class Base {
public:
// 基類(lèi)中可以通過(guò)模板參數(shù)訪問(wèn)派生類(lèi)
void doSomething() {
// 將基類(lèi)指針 this 轉(zhuǎn)換為派生類(lèi)指針
Derived* derived = static_cast<Derived*>(this);
// 調(diào)用派生類(lèi)的成員函數(shù)(編譯期確定)
derived->impl();
}
};
// 2. 派生類(lèi):繼承 Base<自身>
class Derived : public Base<Derived> {
public:
// 派生類(lèi)實(shí)現(xiàn)具體邏輯
void impl() {
cout << "Derived 具體實(shí)現(xiàn)邏輯" << endl;
}
};
int main() {
Derived d;
d.doSomething(); // 輸出:Derived 具體實(shí)現(xiàn)邏輯
return 0;
}
從代碼中可以看到兩個(gè)關(guān)鍵要點(diǎn):
- 基類(lèi)是模板類(lèi),模板參數(shù) Derived\text{Derived}Derived 代表“最終的派生類(lèi)”;
- 派生類(lèi)繼承的是 Base<Derived>\text{Base<Derived>}Base<Derived> ,即把自身作為模板參數(shù)傳入基類(lèi)。
這種語(yǔ)法看似“循環(huán)依賴(lài)”,但 C++ 模板的編譯機(jī)制會(huì)完美處理:模板類(lèi)的實(shí)例化是延遲的,直到真正使用時(shí)才會(huì)展開(kāi),因此基類(lèi)在編譯時(shí)能夠“感知”到派生類(lèi)的存在,從而調(diào)用派生類(lèi)的成員。
本質(zhì):編譯期多態(tài)的實(shí)現(xiàn)載體
很多人對(duì) CRTP 的第一印象是“語(yǔ)法奇特”,但它的本質(zhì)是 編譯期多態(tài)——與虛函數(shù)的“運(yùn)行時(shí)多態(tài)”不同,CRTP 借助模板的編譯期特性,將函數(shù)調(diào)用的綁定時(shí)機(jī)提前到編譯期,從而實(shí)現(xiàn)“零運(yùn)行時(shí)開(kāi)銷(xiāo)”的多態(tài)效果。
我們可以用一個(gè)簡(jiǎn)單的對(duì)比,理解 CRTP 與普通繼承、虛函數(shù)的區(qū)別:
- 普通繼承:基類(lèi)無(wú)法直接訪問(wèn)派生類(lèi)的成員,只能通過(guò)派生類(lèi)對(duì)象調(diào)用自身成員;
- 虛函數(shù)多態(tài):基類(lèi)通過(guò)虛表指針,在運(yùn)行時(shí)查找派生類(lèi)的重寫(xiě)函數(shù),有虛表內(nèi)存開(kāi)銷(xiāo)和函數(shù)調(diào)用開(kāi)銷(xiāo);
- CRTP:基類(lèi)通過(guò)模板參數(shù)“預(yù)知”派生類(lèi)類(lèi)型,編譯期直接綁定派生類(lèi)的成員函數(shù),無(wú)任何運(yùn)行時(shí)開(kāi)銷(xiāo)。
這也是 CRTP 最核心的價(jià)值——在需要多態(tài)特性,且對(duì)性能要求極高的場(chǎng)景(如高頻計(jì)算、嵌入式開(kāi)發(fā))中,替代虛函數(shù),實(shí)現(xiàn)“高效多態(tài)”。
CRTP 的底層原理
要真正吃透 CRTP,必須理解其底層的編譯機(jī)制。我們從“模板實(shí)例化”“類(lèi)型轉(zhuǎn)換”“靜態(tài)綁定”三個(gè)角度,拆解 CRTP 的工作原理。
模板延遲實(shí)例化:解決“循環(huán)依賴(lài)”問(wèn)題
很多人會(huì)疑惑:派生類(lèi)還沒(méi)定義完,怎么能作為模板參數(shù)傳入基類(lèi)?這其實(shí)得益于 C++ 模板的“延遲實(shí)例化”特性。
C++ 模板類(lèi)的成員函數(shù),并不是在模板定義時(shí)就編譯,而是在“首次調(diào)用”時(shí)才會(huì)被實(shí)例化。也就是說(shuō),基類(lèi) Base<Derived>\text{Base<Derived>}Base<Derived> 中,調(diào)用派生類(lèi)成員的代碼(如 derived->impl()\text{derived->impl()}derived->impl() ),直到 Derived\text{Derived}Derived 類(lèi)完全定義、且 doSomething()\text{doSomething()}doSomething() 被調(diào)用時(shí),才會(huì)被編譯。
舉個(gè)反例:如果基類(lèi)的構(gòu)造函數(shù)中直接調(diào)用派生類(lèi)的成員函數(shù),會(huì)編譯失敗——因?yàn)榛?lèi)構(gòu)造函數(shù)在派生類(lèi)構(gòu)造之前執(zhí)行,此時(shí)派生類(lèi)還未初始化,且模板成員函數(shù)的實(shí)例化時(shí)機(jī)提前,會(huì)導(dǎo)致“未定義類(lèi)型”錯(cuò)誤。這一點(diǎn)我們會(huì)在后面的“陷阱”部分詳細(xì)說(shuō)明。
static_cast 轉(zhuǎn)換:編譯期安全的類(lèi)型轉(zhuǎn)換
在 CRTP 基類(lèi)中,我們經(jīng)常會(huì)看到 static_cast<Derived*>}(this) 這樣的轉(zhuǎn)換,這是 CRTP 實(shí)現(xiàn)“基類(lèi)訪問(wèn)派生類(lèi)”的關(guān)鍵。
為什么必須用 static_cast,而不是 dynamic_cast?
- static_cast:編譯期進(jìn)行類(lèi)型轉(zhuǎn)換,無(wú)運(yùn)行時(shí)開(kāi)銷(xiāo),前提是程序員保證轉(zhuǎn)換的安全性(即 this 指針確實(shí)指向 Derived 類(lèi)型的對(duì)象);
- dynamic_cast:運(yùn)行時(shí)進(jìn)行類(lèi)型檢查,有性能開(kāi)銷(xiāo),且需要基類(lèi)有虛函數(shù)(否則無(wú)法使用),而 CRTP 的核心優(yōu)勢(shì)就是“零開(kāi)銷(xiāo)”,因此完全不需要 dynamic_cast。
這里的安全性由程序員保證:只要派生類(lèi)正確繼承了 Base<Derived>\text{Base<Derived>}Base<Derived> ,那么 this 指針指向的就是 Derived 類(lèi)型的對(duì)象,static_cast 轉(zhuǎn)換就是安全的。如果派生類(lèi)繼承時(shí)寫(xiě)錯(cuò)了模板參數(shù)(如 class Derived : public Base<Other>\text{class Derived : public Base<Other>}class Derived : public Base<Other> ),編譯時(shí)會(huì)報(bào)錯(cuò),從而提前發(fā)現(xiàn)問(wèn)題。
靜態(tài)綁定:多態(tài)的零開(kāi)銷(xiāo)實(shí)現(xiàn)
虛函數(shù)的多態(tài)是“動(dòng)態(tài)綁定”:運(yùn)行時(shí)通過(guò)虛表指針,找到當(dāng)前對(duì)象實(shí)際對(duì)應(yīng)的派生類(lèi)函數(shù);而 CRTP 的多態(tài)是“靜態(tài)綁定”:編譯期根據(jù)模板參數(shù),直接確定要調(diào)用的派生類(lèi)函數(shù)。
我們用一段代碼對(duì)比兩者的差異:
#include <iostream>
using namespace std;
// 1. 虛函數(shù)多態(tài)(動(dòng)態(tài)綁定)
class ShapeVirtual {
public:
virtual void draw() = 0; // 純虛函數(shù)
virtual ~ShapeVirtual() {}
};
class CircleVirtual : public ShapeVirtual {
public:
void draw() override {
cout << "繪制圓形(虛函數(shù))" << endl;
}
};
// 2. CRTP 多態(tài)(靜態(tài)綁定)
template <typename Derived>
class ShapeCRTP {
public:
void draw() {
static_cast<Derived*>(this)->drawImpl();
}
};
class CircleCRTP : public ShapeCRTP<CircleCRTP> {
public:
void drawImpl() {
cout << "繪制圓形(CRTP)" << endl;
}
};
int main() {
// 虛函數(shù)多態(tài):運(yùn)行時(shí)綁定
ShapeVirtual* v_ptr = new CircleVirtual();
v_ptr->draw(); // 運(yùn)行時(shí)查找虛表,調(diào)用 CircleVirtual::draw()
// CRTP 多態(tài):編譯期綁定
CircleCRTP c;
c.draw(); // 編譯期直接綁定 CircleCRTP::drawImpl()
delete v_ptr;
return 0;
}
從匯編代碼層面看,虛函數(shù)調(diào)用會(huì)產(chǎn)生“查找虛表”的指令,而 CRTP 的調(diào)用會(huì)直接被編譯成普通函數(shù)調(diào)用,沒(méi)有任何額外開(kāi)銷(xiāo)。這也是 CRTP 在高性能場(chǎng)景中不可或缺的原因。
CRTP 的三大核心用途
CRTP 不僅僅是一種“語(yǔ)法技巧”,更有明確的實(shí)際用途。結(jié)合開(kāi)源框架和實(shí)際開(kāi)發(fā)場(chǎng)景,我們總結(jié)出 CRTP 最常用的三大核心用途,每一種都能大幅提升代碼的效率和可維護(hù)性。
用途一:編譯期多態(tài)——替代虛函數(shù),追求極致性能
當(dāng)我們需要多態(tài)特性,但又無(wú)法接受虛函數(shù)的性能開(kāi)銷(xiāo)(如高頻調(diào)用、嵌入式系統(tǒng)、實(shí)時(shí)計(jì)算)時(shí),CRTP 是最佳選擇。
典型場(chǎng)景:圖形渲染、算法庫(kù)、數(shù)值計(jì)算。例如,我們可以用 CRTP 實(shí)現(xiàn)一個(gè)通用的“算法基類(lèi)”,讓不同的算法派生類(lèi)繼承,實(shí)現(xiàn)編譯期多態(tài)的算法調(diào)用。
#include <iostream>
#include <vector>
using namespace std;
// CRTP 基類(lèi):通用算法接口
template <typename Derived>
class Algorithm {
public:
// 統(tǒng)一接口
void execute(const vector<int>& data) {
static_cast<Derived*>(this)->doExecute(data);
}
};
// 排序算法派生類(lèi)
class SortAlgorithm : public Algorithm<SortAlgorithm> {
public:
void doExecute(const vector<int>& data) {
cout << "執(zhí)行排序算法:";
for (int num : data) cout << num << " ";
cout << endl;
// 實(shí)際排序邏輯...
}
};
// 過(guò)濾算法派生類(lèi)
class FilterAlgorithm : public Algorithm<FilterAlgorithm> {
public:
void doExecute(const vector<int>& data) {
cout << "執(zhí)行過(guò)濾算法:";
for (int num : data) if (num > 5) cout << num << " ";
cout << endl;
// 實(shí)際過(guò)濾邏輯...
}
};
// 通用調(diào)用函數(shù)(編譯期多態(tài))
template <typename T>
void runAlgorithm(Algorithm<T>& algo, const vector<int>& data) {
algo.execute(data);
}
int main() {
vector<int> data = {3, 7, 1, 9, 4};
SortAlgorithm sortAlgo;
FilterAlgorithm filterAlgo;
runAlgorithm(sortAlgo, data); // 執(zhí)行排序算法
runAlgorithm(filterAlgo, data); // 執(zhí)行過(guò)濾算法
return 0;
}
這段代碼中,runAlgorithm 函數(shù)是通用的,能夠接收任何繼承自 Algorithm 的派生類(lèi),且調(diào)用時(shí)沒(méi)有任何虛函數(shù)開(kāi)銷(xiāo)——因?yàn)榫幾g期已經(jīng)確定了要調(diào)用的 doExecute 函數(shù)。
用途二:代碼復(fù)用——注入通用功能,避免重復(fù)編碼
CRTP 最常用的場(chǎng)景之一,是給多個(gè)派生類(lèi)“注入”通用功能,無(wú)需每個(gè)派生類(lèi)重復(fù)實(shí)現(xiàn)。例如:對(duì)象計(jì)數(shù)、日志記錄、序列化、線程安全等。
最經(jīng)典的案例:對(duì)象計(jì)數(shù)。我們可以用 CRTP 實(shí)現(xiàn)一個(gè) Countable 基類(lèi),所有繼承它的派生類(lèi)都會(huì)自動(dòng)獲得“對(duì)象計(jì)數(shù)”功能,無(wú)需自己寫(xiě)構(gòu)造/析構(gòu)函數(shù)來(lái)維護(hù)計(jì)數(shù)。
#include <iostream>
using namespace std;
// CRTP 基類(lèi):提供對(duì)象計(jì)數(shù)功能
template <typename Derived>
class Countable {
public:
// 靜態(tài)變量:記錄當(dāng)前派生類(lèi)的對(duì)象個(gè)數(shù)
static inline int count = 0;
// 構(gòu)造函數(shù):對(duì)象創(chuàng)建時(shí)計(jì)數(shù)+1
Countable() {
count++;
}
// 拷貝構(gòu)造:同樣計(jì)數(shù)+1
Countable(const Countable&) {
count++;
}
// 析構(gòu)函數(shù):對(duì)象銷(xiāo)毀時(shí)計(jì)數(shù)-1
~Countable() {
count--;
}
// 靜態(tài)方法:獲取當(dāng)前對(duì)象個(gè)數(shù)
static int getCount() {
return count;
}
};
// 派生類(lèi) A:自動(dòng)獲得計(jì)數(shù)功能
class A : public Countable<A> {};
// 派生類(lèi) B:自動(dòng)獲得計(jì)數(shù)功能
class B : public Countable<B> {};
int main() {
A a1, a2;
B b1;
cout << "A 類(lèi)對(duì)象個(gè)數(shù):" << A::getCount() << endl; // 輸出:2
cout << "B 類(lèi)對(duì)象個(gè)數(shù):" << B::getCount() << endl; // 輸出:1
{
A a3;
cout << "A 類(lèi)對(duì)象個(gè)數(shù)(局部作用域):" << A::getCount() << endl; // 輸出:3
}
cout << "A 類(lèi)對(duì)象個(gè)數(shù)(局部作用域結(jié)束):" << A::getCount() << endl; // 輸出:2
return 0;
}
這里有一個(gè)關(guān)鍵細(xì)節(jié): Countable<A>\text{Countable<A>}Countable<A> 和 Countable<B>\text{Countable<B>}Countable<B> 是兩個(gè)完全不同的模板實(shí)例化類(lèi),它們的靜態(tài)變量 count 是獨(dú)立的——這意味著,A 類(lèi)和 B 類(lèi)的計(jì)數(shù)互不影響,這正是我們想要的效果。
除了對(duì)象計(jì)數(shù),我們還可以用 CRTP 實(shí)現(xiàn)“日志注入”:給所有派生類(lèi)自動(dòng)添加日志記錄功能,記錄函數(shù)調(diào)用、參數(shù)等信息,無(wú)需每個(gè)派生類(lèi)重復(fù)寫(xiě)日志代碼。
用途三:編譯期檢查——禁止類(lèi)被繼承(替代 final)
C++11 引入了 final 關(guān)鍵字,可以禁止類(lèi)被繼承,但在 C++11 之前,我們可以用 CRTP 實(shí)現(xiàn)“不可派生類(lèi)”,且比 final 更靈活(例如可以結(jié)合其他功能)。
實(shí)現(xiàn)原理:將基類(lèi)的構(gòu)造函數(shù)和析構(gòu)函數(shù)設(shè)為私有,派生類(lèi)通過(guò) CRTP 繼承后,只能通過(guò)基類(lèi)的 public 接口創(chuàng)建對(duì)象,而無(wú)法被其他類(lèi)繼承(因?yàn)槔^承后無(wú)法訪問(wèn)基類(lèi)的構(gòu)造函數(shù))。
#include <iostream>
using namespace std;
// CRTP 基類(lèi):禁止派生類(lèi)被繼承
template <typename Derived>
class NonInheritable {
private:
// 私有構(gòu)造函數(shù)和析構(gòu)函數(shù)
NonInheritable() {}
~NonInheritable() {}
// 允許 Derived 訪問(wèn)私有成員
friend class Derived;
};
// 可實(shí)例化,但不能被繼承
class FinalClass : public NonInheritable<FinalClass> {
public:
void print() {
cout << "FinalClass 不能被繼承" << endl;
}
};
// 嘗試?yán)^承 FinalClass:編譯失?。?
// class SubClass : public FinalClass {
// };
int main() {
FinalClass fc;
fc.print(); // 正常運(yùn)行
return 0;
}
這段代碼中,F(xiàn)inalClass 可以正常實(shí)例化,但如果有類(lèi)嘗試?yán)^承它,會(huì)因?yàn)闊o(wú)法訪問(wèn) NonInheritable 的私有構(gòu)造函數(shù)而編譯失敗。這種方式兼容 C++11 之前的標(biāo)準(zhǔn),是早期 C++ 中實(shí)現(xiàn)“不可派生類(lèi)”的常用技巧。
CRTP 進(jìn)階
掌握了 CRTP 的基礎(chǔ)用法后,我們可以結(jié)合模板元編程、多重繼承等特性,實(shí)現(xiàn)更復(fù)雜的功能。以下是兩個(gè)常用的進(jìn)階場(chǎng)景。
多重 CRTP——繼承多個(gè) CRTP 基類(lèi),注入多種功能
一個(gè)派生類(lèi)可以同時(shí)繼承多個(gè) CRTP 基類(lèi),從而獲得多種通用功能。例如,一個(gè)類(lèi)可以同時(shí)繼承“對(duì)象計(jì)數(shù)”和“日志記錄”兩個(gè) CRTP 基類(lèi),無(wú)需自己實(shí)現(xiàn)任何代碼。
#include <iostream>
#include <string>
using namespace std;
// 1. CRTP 基類(lèi)1:對(duì)象計(jì)數(shù)
template <typename Derived>
class Countable {
public:
static inline int count = 0;
Countable() { count++; }
~Countable() { count--; }
static int getCount() { return count; }
};
// 2. CRTP 基類(lèi)2:日志記錄
template <typename Derived>
class Loggable {
public:
void log(const string& msg) {
cout << "[" << typeid(Derived).name() << "] " << msg << endl;
}
};
// 派生類(lèi):同時(shí)繼承兩個(gè) CRTP 基類(lèi),獲得計(jì)數(shù)和日志功能
class MyClass : public Countable<MyClass>, public Loggable<MyClass> {
public:
void doWork() {
log("開(kāi)始執(zhí)行 doWork");
// 業(yè)務(wù)邏輯...
log("doWork 執(zhí)行完畢");
}
};
int main() {
MyClass c1, c2;
c1.doWork();
c2.doWork();
cout << "MyClass 對(duì)象個(gè)數(shù):" << MyClass::getCount() << endl; // 輸出:2
return 0;
}
這里需要注意:多重繼承時(shí),兩個(gè) CRTP 基類(lèi)都是模板類(lèi),且模板參數(shù)都是 MyClass,因此不會(huì)產(chǎn)生歧義——因?yàn)?Countable 和 Loggable 是兩個(gè)不同的類(lèi),它們的成員函數(shù)互不沖突。
CRTP 與模板元編程(TMP)——編譯期計(jì)算
CRTP 常與模板元編程結(jié)合,實(shí)現(xiàn)編譯期計(jì)算、類(lèi)型判斷等功能。例如,我們可以用 CRTP 實(shí)現(xiàn)一個(gè)“編譯期計(jì)數(shù)器”,在編譯期確定某個(gè)類(lèi)型的實(shí)例個(gè)數(shù)。
#include <iostream>
using namespace std;
// CRTP 基類(lèi):編譯期計(jì)數(shù)器
template <typename Derived, int N = 0>
class CompileTimeCounter {
public:
// 編譯期常量:當(dāng)前計(jì)數(shù)
static constexpr int value = N;
// 派生類(lèi):計(jì)數(shù)+1
template <int M = N + 1>
using Next = CompileTimeCounter<Derived, M>;
};
// 測(cè)試:編譯期計(jì)數(shù)
class Test;
using Counter1 = CompileTimeCounter<Test>; // value = 0
using Counter2 = Counter1::Next; // value = 1
using Counter3 = Counter2::Next; // value = 2
int main() {
// 編譯期確定的值,直接輸出
cout << Counter1::value << endl; // 輸出:0
cout << Counter2::value << endl; // 輸出:1
cout << Counter3::value << endl; // 輸出:2
return 0;
}
這段代碼中,計(jì)數(shù)器的 value 是編譯期常量,在編譯時(shí)就已經(jīng)確定,運(yùn)行時(shí)無(wú)需任何計(jì)算——這就是模板元編程的核心思想,而 CRTP 為這種編譯期計(jì)算提供了簡(jiǎn)潔的實(shí)現(xiàn)載體。
CRTP 常見(jiàn)陷阱與避坑指南
CRTP 雖然強(qiáng)大,但也有很多容易踩坑的地方。以下是開(kāi)發(fā)中最常見(jiàn)的 4 個(gè)陷阱,以及對(duì)應(yīng)的避坑技巧,幫你避免低級(jí)錯(cuò)誤。
陷阱 1:在基類(lèi)構(gòu)造/析構(gòu)函數(shù)中調(diào)用派生類(lèi)成員
派生類(lèi)的構(gòu)造順序是:先構(gòu)造基類(lèi),再構(gòu)造派生類(lèi);析構(gòu)順序則相反:先析構(gòu)派生類(lèi),再析構(gòu)基類(lèi)。因此,在基類(lèi)的構(gòu)造/析構(gòu)函數(shù)中調(diào)用派生類(lèi)的成員函數(shù),會(huì)導(dǎo)致“未初始化的對(duì)象訪問(wèn)”,從而引發(fā)崩潰。
錯(cuò)誤示例:
template <typename Derived>
class Base {
public:
Base() {
// 錯(cuò)誤:基類(lèi)構(gòu)造時(shí),派生類(lèi)還未初始化
static_cast<Derived*>(this)->impl();
}
};
class Derived : public Base<Derived> {
public:
int data;
void impl() {
data = 10; // 訪問(wèn)未初始化的 data,行為未定義
}
};
避坑技巧:永遠(yuǎn)不要在 CRTP 基類(lèi)的構(gòu)造/析構(gòu)函數(shù)中調(diào)用派生類(lèi)的成員函數(shù)。如果需要初始化,可以提供一個(gè)獨(dú)立的 init 方法,在派生類(lèi)構(gòu)造完成后手動(dòng)調(diào)用。
陷阱 2:使用 dynamic_cast 進(jìn)行類(lèi)型轉(zhuǎn)換
有些開(kāi)發(fā)者習(xí)慣用 dynamic_cast 進(jìn)行類(lèi)型轉(zhuǎn)換,認(rèn)為這樣更“安全”,但在 CRTP 中,dynamic_cast 不僅沒(méi)有必要,還會(huì)引入性能開(kāi)銷(xiāo),甚至無(wú)法使用(如果基類(lèi)沒(méi)有虛函數(shù))。
避坑技巧:CRTP 中,基類(lèi)到派生類(lèi)的轉(zhuǎn)換,必須用 static_cast——只要派生類(lèi)正確繼承了 Base,static_cast 就是安全的,且無(wú)運(yùn)行時(shí)開(kāi)銷(xiāo)。
陷阱 3:誤以為 CRTP 可以實(shí)現(xiàn)運(yùn)行時(shí)多態(tài)數(shù)組
虛函數(shù)多態(tài)的一個(gè)重要優(yōu)勢(shì)是:可以用基類(lèi)指針數(shù)組存儲(chǔ)不同的派生類(lèi)對(duì)象,實(shí)現(xiàn)運(yùn)行時(shí)動(dòng)態(tài)切換。但 CRTP 是編譯期多態(tài),無(wú)法實(shí)現(xiàn)這一功能。
錯(cuò)誤示例:
template <typename Derived>
class Base {};
class Derived1 : public Base<Derived1> {};
class Derived2 : public Base<Derived2> {};
int main() {
// 錯(cuò)誤:Base<Derived1> 和 Base<Derived2> 是不同的類(lèi),無(wú)法存入同一個(gè)數(shù)組
// Base<Derived1>* arr[] = {new Derived1(), new Derived2()};
return 0;
}
避坑技巧:如果需要運(yùn)行時(shí)多態(tài)數(shù)組,使用虛函數(shù);如果追求性能,且不需要?jiǎng)討B(tài)切換類(lèi)型,使用 CRTP。兩者各有適用場(chǎng)景,不要混淆。
陷阱 4:派生類(lèi)繼承時(shí)寫(xiě)錯(cuò)模板參數(shù)
CRTP 的核心是“派生類(lèi)作為基類(lèi)的模板參數(shù)”,如果派生類(lèi)繼承時(shí)寫(xiě)錯(cuò)了模板參數(shù)(如寫(xiě)成 Base),會(huì)導(dǎo)致編譯錯(cuò)誤,或運(yùn)行時(shí)行為異常。
錯(cuò)誤示例:
template <typename Derived>
class Base {
public:
void call() {
static_cast<Derived*>(this)->impl();
}
};
class Derived1 : public Base<Derived1> { // 正確
public:
void impl() { cout << "Derived1" << endl; }
};
class Derived2 : public Base<Derived1> { // 錯(cuò)誤:模板參數(shù)寫(xiě)錯(cuò)
public:
void impl() { cout << "Derived2" << endl; }
};
避坑技巧:派生類(lèi)繼承時(shí),模板參數(shù)必須是自身。可以通過(guò) IDE 的代碼提示、編譯時(shí)報(bào)錯(cuò)(如“沒(méi)有成員 impl”)及時(shí)發(fā)現(xiàn)問(wèn)題。
CRTP 和 虛函數(shù)
很多開(kāi)發(fā)者會(huì)糾結(jié):什么時(shí)候用 CRTP,什么時(shí)候用虛函數(shù)?我們通過(guò)一張表格,全面對(duì)比兩者的特性,幫你做出合理選擇。
| 特性 | CRTP(靜態(tài)多態(tài)) | 虛函數(shù)(動(dòng)態(tài)多態(tài)) |
|---|---|---|
| 綁定時(shí)機(jī) | 編譯期靜態(tài)綁定 | 運(yùn)行時(shí)動(dòng)態(tài)綁定 |
| 性能開(kāi)銷(xiāo) | 零運(yùn)行時(shí)開(kāi)銷(xiāo),與普通函數(shù)一致 | 虛表查找、函數(shù)調(diào)用開(kāi)銷(xiāo),額外的虛表內(nèi)存開(kāi)銷(xiāo) |
| 靈活性 | 編譯期確定類(lèi)型,無(wú)法動(dòng)態(tài)切換 | 運(yùn)行時(shí)動(dòng)態(tài)切換類(lèi)型,靈活性高 |
| 代碼復(fù)用 | 強(qiáng),可通過(guò)模板注入通用功能 | 中,需通過(guò)虛函數(shù)重寫(xiě)實(shí)現(xiàn) |
| 數(shù)組存儲(chǔ) | 不支持(基類(lèi)模板實(shí)例不同) | 支持(基類(lèi)指針數(shù)組) |
| 適用場(chǎng)景 | 高性能場(chǎng)景、通用功能注入、編譯期檢查 | 需要?jiǎng)討B(tài)切換類(lèi)型、靈活性?xún)?yōu)先的場(chǎng)景 |
| 總結(jié):沒(méi)有絕對(duì)的“誰(shuí)更好”,只有“誰(shuí)更合適”。在追求極致性能、不需要?jiǎng)討B(tài)切換類(lèi)型的場(chǎng)景中,CRTP 是首選;在需要運(yùn)行時(shí)動(dòng)態(tài)切換類(lèi)型、靈活性更重要的場(chǎng)景中,虛函數(shù)更合適。 |
案例——CRTP 在開(kāi)源框架中的應(yīng)用
CRTP 并不是紙上談兵,在很多知名開(kāi)源框架中都有廣泛應(yīng)用。了解這些實(shí)戰(zhàn)案例,能幫助我們更好地理解 CRTP 的實(shí)際價(jià)值。
案例 1:Boost 庫(kù)中的 CRTP 應(yīng)用
Boost 庫(kù)中,很多組件都用到了 CRTP 實(shí)現(xiàn)代碼復(fù)用和性能優(yōu)化。例如,Boost.NonCopyable 類(lèi)(禁止對(duì)象拷貝)的底層的實(shí)現(xiàn),就用到了 CRTP 的思想——通過(guò)模板參數(shù)注入“不可拷貝”的功能。
簡(jiǎn)化版實(shí)現(xiàn):
template <typename Derived>
class NonCopyable {
protected:
NonCopyable() {}
~NonCopyable() {}
private:
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
};
// 禁止拷貝的類(lèi)
class MyClass : public NonCopyable<MyClass> {};
案例 2:STL 中的 CRTP 應(yīng)用
STL 中的迭代器(Iterator)也用到了 CRTP。例如,STL 中的 iterator_traits(迭代器特性萃取),通過(guò) CRTP 實(shí)現(xiàn)了對(duì)不同類(lèi)型迭代器的統(tǒng)一接口封裝,同時(shí)保證了高性能。
核心思想:將迭代器類(lèi)型作為模板參數(shù)傳入基類(lèi),基類(lèi)提供統(tǒng)一的接口,派生類(lèi)實(shí)現(xiàn)具體的迭代器邏輯,從而實(shí)現(xiàn)編譯期多態(tài)的迭代器操作。
到此這篇關(guān)于C++奇異遞歸模板(CRTP)的使用小結(jié)的文章就介紹到這了,更多相關(guān)C++奇異遞歸模板(CRTP)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)圖書(shū)管理系統(tǒng)課程設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)圖書(shū)管理系統(tǒng)課程設(shè)計(jì),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Qt使用QChart實(shí)現(xiàn)動(dòng)態(tài)顯示溫度變化曲線
Qt的QChart是一個(gè)用于繪制圖表和可視化數(shù)據(jù)的類(lèi),提供了一個(gè)靈活的、可擴(kuò)展的、跨平臺(tái)的圖表繪制解決方案,所以本文就將使用QChart實(shí)現(xiàn)動(dòng)態(tài)顯示3個(gè)設(shè)備的溫度變化曲線,感興趣的可以了解一下2023-06-06
Qt使用Qt Linguist 實(shí)現(xiàn)多語(yǔ)言切換
本文主要介紹了Qt使用Qt Linguist 實(shí)現(xiàn)多語(yǔ)言切換,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2026-07-07
C語(yǔ)言對(duì)磁盤(pán)文件進(jìn)行快速排序簡(jiǎn)單實(shí)例
這篇文章主要介紹了C語(yǔ)言對(duì)磁盤(pán)文件進(jìn)行快速排序簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06

