C++ CRTP(奇異遞歸模板模式)
CRTP 是什么?
一句話總結:CRTP 就是讓子類把自己作為模板參數傳遞給父類。
聽起來有點繞,直接上代碼就明白了:
template <typename Derived>
class Base {// ...
};class Derived : public Base<Derived> {// ...
};
Derived
繼承自 Base<Derived>
,也就是說,子類把自己“遞歸”地傳給了父類。這就是“奇異遞歸”的由來。
CRTP 有啥用?
其實 CRTP 最常用的場景有三個:
- 靜態多態:不用虛函數也能實現類似多態的效果,而且沒有虛表,效率高。
- 代碼復用:基類寫通用邏輯,子類只需要實現自己的部分。
- 每個子類獨立的靜態成員:比如計數器,每個子類都有自己的靜態變量。
CRTP 的原理
CRTP 的核心原理其實很簡單,就是利用了 C++ 模板的“編譯期展開”特性,讓基類在編譯時就能知道派生類的類型。
1. static_cast 的作用
在 CRTP 里,基類通常會用 static_cast<Derived*>(this)
把自己轉換成派生類指針,然后調用派生類的方法。這樣,雖然代碼寫在基類里,但實際調用的是派生類的實現。
比如:
void interface() {static_cast<Derived*>(this)->implementation();
}
這行代碼在編譯期就能確定 Derived
的類型,所以沒有虛表,也沒有運行時開銷。
2. 編譯期多態的本質
CRTP 實現的是“靜態多態”,也就是多態的分發發生在編譯期,而不是運行時。模板展開時,基類里的 static_cast<Derived*>
會被替換成具體的派生類類型,所有調用都在編譯時就確定了。
3. 代碼復用和靜態接口約束
- 代碼復用:基類可以寫通用的邏輯,比如日志、計數、接口包裝等,具體實現交給派生類。這樣不同的派生類可以復用同一套基類邏輯。
- 靜態接口約束:如果派生類沒有實現基類里要調用的方法(比如
implementation()
),編譯時就會報錯。這其實是一種“編譯期接口檢查”,比傳統的虛函數更早發現問題。
一個簡單的例子
假如我有一堆不同的動物,每種動物都能“說話”,但我又不想用虛函數(比如對性能有要求),CRTP 就能派上用場:
#include <iostream>template <typename Derived>
class Animal {
public:void speak() {static_cast<Derived*>(this)->speak_impl();}
};class Dog : public Animal<Dog> {
public:void speak_impl() {std::cout << "汪汪!" << std::endl;}
};class Cat : public Animal<Cat> {
public:void speak_impl() {std::cout << "喵喵!" << std::endl;}
};int main() {Dog d;Cat c;d.speak(); // 汪汪!c.speak(); // 喵喵!return 0;
}
這里的 Animal
基類里有個 speak()
,但真正的實現是在子類里。通過 static_cast<Derived*>(this)
,基類可以“靜態”地調用子類的方法。這樣既有多態的效果,又沒有虛函數的開銷。
CRTP 實例
1. 每個子類獨立計數
有時候我想統計每種類型各自創建了多少對象,CRTP 也能輕松搞定:
template <typename Derived>
class Counter {
public:static int count;Counter() { ++count; }
};
template <typename Derived>
int Counter<Derived>::count = 0;class Apple : public Counter<Apple> {};
class Banana : public Counter<Banana> {};int main() {Apple a1, a2;Banana b1;std::cout << Apple::count << std::endl; // 輸出2std::cout << Banana::count << std::endl; // 輸出1
}
每個子類都有自己的靜態成員變量,互不影響。
2 . 日志
#include <iostream>
#include <string>// CRTP 日志基類
template <typename Derived>
class LoggerBase {
public:void runWithLog(const std::string& opName) {std::cout << "[LOG] 開始操作: " << opName << std::endl;static_cast<Derived*>(this)->run(); // 調用派生類的 run()std::cout << "[LOG] 結束操作: " << opName << std::endl;}
};// 業務類A
class MyAlgorithm : public LoggerBase<MyAlgorithm> {
public:void run() {std::cout << "算法A正在運行..." << std::endl;}
};// 業務類B
class MyService : public LoggerBase<MyService> {
public:void run() {std::cout << "服務B正在處理..." << std::endl;}
};int main() {MyAlgorithm algo;MyService svc;algo.runWithLog("算法A任務");svc.runWithLog("服務B任務");return 0;
}
CRTP 和虛函數的區別
- 虛函數:運行時多態,有虛表指針,靈活但有點性能損耗。
- CRTP:編譯期多態,沒有虛表,效率高,但只能在編譯期確定類型。