C++ 中的 CRTP(奇异递归模板模式)
CRTP(Curiously Recurring Template Pattern)是一种利用模板继承实现 静态多态(Static Polymorphism) 的设计模式。通过基类模板以派生类作为模板参数,CRTP 允许在编译期实现多态行为,避免虚函数开销,同时提供灵活的类型操作。以下通过代码和底层原理全面解析其实现和应用。
1. CRTP 的基本结构
1.1 核心思想
- 基类模板:接受派生类作为模板参数。
- 派生类:继承自以自身为模板参数的基类模板。
// 基类模板
template <typename Derived>
class Base {
public:// 通过静态转换访问派生类Derived& derived() { return static_cast<Derived&>(*this); }
};// 派生类继承自以自身为模板参数的基类
class MyDerived : public Base<MyDerived> {// 派生类的实现
};
2. CRTP 的典型应用场景
2.1 静态多态(替代虚函数)
template <typename Derived>
class Shape {
public:void draw() {// 调用派生类的具体实现static_cast<Derived*>(this)->drawImpl();}
};class Circle : public Shape<Circle> {
public:void drawImpl() { std::cout << "Drawing Circle" << std::endl; }
};class Square : public Shape<Square> {
public:void drawImpl() { std::cout << "Drawing Square" << std::endl; }
};int main() {Circle circle;Square square;circle.draw(); // 输出 "Drawing Circle"square.draw(); // 输出 "Drawing Square"
}
底层原理:
- 通过
static_cast
在编译期将基类指针转换为派生类指针,直接调用具体实现。 - 无虚函数表(vtable)开销,性能更高。
2.2 接口扩展与代码复用
template <typename Derived>
class Addable {
public:Derived operator+(const Derived& other) const {Derived result = static_cast<const Derived&>(*this);result += other;return result;}
};class Vector : public Addable<Vector> {
public:int x, y;Vector(int x, int y) : x(x), y(y) {}Vector& operator+=(const Vector& other) {x += other.x;y += other.y;return *this;}
};int main() {Vector v1(1, 2), v2(3, 4);Vector v3 = v1 + v2; // 使用基类的 operator+ 实现std::cout << v3.x << ", " << v3.y << std::endl; // 输出 "4, 6"
}
2.3 对象计数(编译期统计)
template <typename Derived>
class ObjectCounter {
public:static int count;ObjectCounter() { count++; }~ObjectCounter() { count--; }
};template <typename Derived>
int ObjectCounter<Derived>::count = 0;class MyClass : public ObjectCounter<MyClass> {};int main() {MyClass a, b;std::cout << MyClass::count << std::endl; // 输出 "2"{MyClass c;std::cout << MyClass::count << std::endl; // 输出 "3"}std::cout << MyClass::count << std::endl; // 输出 "2"
}
3. CRTP 的底层原理
3.1 模板实例化与类型推导
- 基类模板参数:在派生类定义时,将自身类型传递给基类模板。
- 静态类型转换:通过
static_cast
在编译期完成派生类类型的解析。
3.2 符号生成与名称修饰
- 每个派生类实例化基类模板时,生成独立的符号。
- 例如:
Base<Circle>
→_4BaseI6CircleE
Base<Square>
→_4BaseI6SquareE
4. CRTP 的优缺点
优点 | 缺点 |
---|---|
无虚函数开销,性能更高 | 代码可读性较低,设计复杂度较高 |
编译期多态,避免运行时类型检查 | 派生类需明确知晓基类模板的实现细节 |
灵活的类型操作(如运算符重载、静态接口扩展) | 模板错误信息可能难以调试 |
5. CRTP 的高级应用
5.1 链式调用(Fluent Interface)
template <typename Derived>
class Chainable {
public:Derived& self() { return static_cast<Derived&>(*this); }Derived& setName(const std::string& name) {self().name = name;return self();}Derived& setValue(int value) {self().value = value;return self();}
};class Config : public Chainable<Config> {
public:std::string name;int value;
};int main() {Config config;config.setName("MyApp").setValue(42);std::cout << config.name << " " << config.value << std::endl; // 输出 "MyApp 42"
}
5.2 编译期策略模式
template <typename Derived>
class SortingPolicy {
public:void sort(int* data, size_t size) {static_cast<Derived*>(this)->sortImpl(data, size);}
};class QuickSort : public SortingPolicy<QuickSort> {
public:void sortImpl(int* data, size_t size) {std::cout << "QuickSort" << std::endl;// 具体实现}
};class MergeSort : public SortingPolicy<MergeSort> {
public:void sortImpl(int* data, size_t size) {std::cout << "MergeSort" << std::endl;// 具体实现}
};int main() {int arr[5] = {5, 3, 1, 4, 2};QuickSort sorter;sorter.sort(arr, 5); // 输出 "QuickSort"
}
6. 总结
场景 | 技术要点 |
---|---|
静态多态 | 通过 static_cast 调用派生类方法,避免虚函数开销 |
接口扩展 | 基类模板提供通用操作(如 operator+ ),派生类实现具体逻辑(如 operator+= ) |
编译期对象计数 | 利用静态成员变量统计实例数量 |
链式调用 | 返回派生类引用实现链式操作 |
策略模式 | 基类定义策略接口,派生类实现具体策略 |
CRTP 通过模板继承和编译期类型转换,将多态行为提前到编译期处理,适用于高性能、低延迟场景。合理使用可提升代码复用性和灵活性,但需注意设计复杂度和可维护性。
多选题
题目 1:CRTP 基类如何访问派生类的方法?
以下代码的输出是什么?
template <typename Derived>
class Base {
public:void execute() {static_cast<Derived*>(this)->impl();}
};class Derived : public Base<Derived> {
public:void impl() { std::cout << "Derived impl" << std::endl; }
};int main() {Derived d;d.execute();return 0;
}
A. 输出 Derived impl
B. 编译失败,Base
无法访问 Derived
的 impl()
C. 运行时错误,类型转换失败
D. 输出未定义行为
题目 2:静态多态与动态多态的性能对比
以下关于 CRTP 的说法,哪一项正确?
A. CRTP 通过虚函数表实现多态,性能与动态多态相同
B. CRTP 在编译期解析方法调用,性能优于动态多态
C. CRTP 必须在运行时通过 RTTI 检查类型
D. CRTP 的性能不如动态多态,因为模板实例化开销大
题目 3:CRTP 实现对象计数的行为
以下代码的输出是什么?
template <typename Derived>
class Counter {
public:static int count;Counter() { count++; }~Counter() { count--; }
};template <typename Derived>
int Counter<Derived>::count = 0;class WidgetA : public Counter<WidgetA> {};
class WidgetB : public Counter<WidgetB> {};int main() {WidgetA a1, a2;WidgetB b1;std::cout << WidgetA::count << " " << WidgetB::count << std::endl;return 0;
}
A. 2 1
B. 3 1
C. 2 0
D. 1 1
题目 4:CRTP 链式调用的实现
以下代码是否能编译通过?
template <typename Derived>
class Chainable {
public:Derived& setX(int x) {static_cast<Derived*>(this)->x = x;return *static_cast<Derived*>(this);}
};class Point : public Chainable<Point> {
public:int x, y;Point& setY(int y) { this->y = y; return *this; }
};int main() {Point p;p.setX(10).setY(20);return 0;
}
A. 编译成功
B. 编译失败,setX
返回类型错误
C. 编译失败,Point
未继承 Chainable
的正确版本
D. 运行时错误
题目 5:CRTP 与模板特化的交互
以下代码的输出是什么?
template <typename Derived>
class Base {
public:void print() { std::cout << "Base" << std::endl; }
};template <>
class Base<int> {
public:void print() { std::cout << "Base<int>" << std::endl; }
};class Derived : public Base<Derived> {
public:void print() { std::cout << "Derived" << std::endl; }
};int main() {Derived d;d.print();return 0;
}
A. 输出 Base
B. 输出 Derived
C. 输出 Base<int>
D. 编译失败,Derived
无法继承 Base<Derived>
答案与解析
题目 1:CRTP 基类如何访问派生类的方法?
答案:A
解析:
- CRTP 的基类通过
static_cast<Derived*>(this)
将this
指针转换为派生类指针,直接调用impl()
。 - 选项 B 错误,因为 CRTP 基类与派生类是模板继承关系,编译期即可解析方法调用。
题目 2:静态多态与动态多态的性能对比
答案:B
解析:
- CRTP 的静态多态在编译期完成方法绑定,无需虚函数表查找,性能更高。
- 选项 A 错误,CRTP 不依赖虚函数表;选项 C 和 D 均与 CRTP 的机制矛盾。
题目 3:CRTP 实现对象计数的行为
答案:A
解析:
WidgetA
和WidgetB
分别继承自不同的Counter
实例化模板,因此它们的静态成员count
是独立的。WidgetA
构造两次,count
为 2;WidgetB
构造一次,count
为 1。
题目 4:CRTP 链式调用的实现
答案:A
解析:
Chainable::setX
返回Derived&
(即Point&
),因此p.setX(10)
返回Point
对象,支持链式调用setY(20)
。- 选项 B 错误,返回类型正确;选项 C 和 D 无依据。
题目 5:CRTP 与模板特化的交互
答案:B
解析:
Derived
继承自Base<Derived>
,未使用Base<int>
的特化版本。Derived::print()
隐藏了基类的print()
,因此直接调用派生类方法。- 选项 C 错误,
Derived
的基类是Base<Derived>
,而非Base<int>
。