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:编译期多态,没有虚表,效率高,但只能在编译期确定类型。