在看代码时遇到这样一种写法,不是很理解,把模版类自己用作模版参数:
1
|
class MyStubService : public DbusServer<MyStubService> {};
|
咨询了 DeepSeek 后得知这是 C++ 中的 CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)。
CRTP 是一种借助模板实现编译时多态的技术,核心思想是:类模板以其派生类作为模板参数。
CRTP 的优势¶
- 性能优势:避免了虚函数调用的运行时开销
- 编译时优化:编译器可以进行更好的内联和优化
- 类型安全:在编译时捕获类型错误
- 减少二进制大小:不需要虚函数表
CRTP 基本语法结构¶
1
2
3
4
5
6
7
8
|
template <typename Derived>
class Base {
// 基类定义
};
class Derived : public Base<Derived> { // 关键:派生类将自己作为模板参数传递给基类
// 派生类定义
};
|
CRTP 主要应用场景¶
1. 静态多态(编译时多态)¶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
template <typename Derived>
class Animal {
public:
void speak() {
static_cast<Derived*>(this)->speakImpl(); // 编译时绑定
}
void run() {
static_cast<Derived*>(this)->runImpl();
}
};
class Dog : public Animal<Dog> {
public:
void speakImpl() {
std::cout << "Woof!" << std::endl;
}
void runImpl() {
std::cout << "Running on four legs" << std::endl;
}
};
class Cat : public Animal<Cat> {
public:
void speakImpl() {
std::cout << "Meow!" << std::endl;
}
void runImpl() {
std::cout << "Running gracefully" << std::endl;
}
};
// 使用
template <typename T>
void makeAnimalSpeak(Animal<T>& animal) {
animal.speak(); // 编译时确定调用哪个实现
}
|
2. 对象计数¶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
template <typename T>
class Counter {
private:
static int count;
protected:
Counter() { ++count; }
Counter(const Counter&) { ++count; }
~Counter() { --count; }
public:
static int getCount() { return count; }
};
// 静态成员初始化
template <typename T>
int Counter<T>::count = 0;
// 使用
class MyClass : public Counter<MyClass> {
// 自动获得计数功能
};
class AnotherClass : public Counter<AnotherClass> {
// 独立的计数
};
|
3. 链式调用¶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
template <typename Derived>
class Chainable {
public:
Derived& self() { return static_cast<Derived&>(*this); }
Derived& setX(int x) {
// 具体实现由派生类提供
self().setXImpl(x);
return self(); // 返回派生类引用,支持链式调用
}
Derived& setY(int y) {
self().setYImpl(y);
return self();
}
};
class Point : public Chainable<Point> {
private:
int x, y;
public:
Point& setXImpl(int x_) { x = x_; return *this; }
Point& setYImpl(int y_) { y = y_; return *this; }
void print() const {
std::cout << "Point(" << x << ", " << y << ")" << std::endl;
}
};
// 使用:链式调用
Point p;
p.setX(10).setY(20).print();
|
4. 实现运算符重载¶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
template <typename Derived>
class Comparable {
public:
bool operator==(const Derived& other) const {
return static_cast<const Derived*>(this)->equals(other);
}
bool operator!=(const Derived& other) const {
return !static_cast<const Derived*>(this)->equals(other);
}
};
class Person : public Comparable<Person> {
private:
std::string name;
int age;
public:
Person(const std::string& n, int a) : name(n), age(a) {}
bool equals(const Person& other) const {
return name == other.name && age == other.age;
}
};
|
5. 静态接口检查(约束派生类的必备成员)¶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
template <typename Derived>
class MustHaveFoo {
public:
void callFoo() {
static_assert(std::is_same_v<decltype(&Derived::foo), void(Derived::*)()>,
"Derived must define void foo() member");
static_cast<Derived*>(this)->foo();
}
};
class OK : public MustHaveFoo<OK> {
public:
void foo() {}
};
|
也可用 C++20 Concepts 直接约束:
1
2
3
4
5
|
template <typename T>
concept HasFoo = requires(T t) { t.foo(); };
template <HasFoo Derived>
class MustHaveFoo2 { /* ... */ };
|
6. 空基类优化(EBO)¶
CRTP 基类常为空(仅编译期行为),作为空基类被继承时通常不占用额外空间,有助于零开销地叠加特性。
7. Barton–Nackman 技巧(基于 CRTP 的友元运算符)¶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
template <typename Derived>
class Orderable {
public:
friend bool operator<(const Derived& a, const Derived& b) {
return a.less(b);
}
};
class Node : public Orderable<Node> {
public:
bool less(const Node& other) const { return key < other.key; }
private:
int key{0};
};
|
CRTP 的局限性¶
- 编译时绑定:不能在运行时动态改变行为
- 模板复杂性:错误信息可能难以理解
- 代码膨胀:每个派生类都会实例化一个不同的基类模板
最佳实践与常见陷阱¶
- 只在需要零开销和编译期已知类型时使用,否则优先简单的虚函数或组合。
- 基类中用
static_cast<Derived*>/static_cast<const Derived*> 调用派生实现;不要用 dynamic_cast。
- 避免通过基类指针/引用“多态地”存放不同派生对象(CRTP 不是为此设计)。
- 多重继承下的 CRTP:注意菱形结构与方法可见性,必要时用
using Derived::method 暴露成员。
- 为 CRTP 基类提供
protected 构造函数,避免被直接实例化。
- 若需接口约束,结合
static_assert 或 Concepts,尽早在编译期报错。
- 链式 API 返回
Derived&/const Derived&,避免返回 *this 的基类引用。
何时使用 / 不使用 CRTP¶
- 使用:
- 静态多态、热点路径需要最大化内联与去虚拟化。
- 可复用的“mixin 特性”(日志、计数、对比、序列化接口)按需叠加。
- 需要在编译期强制派生类提供某些接口(静态约束)。
- 不使用:
- 需要在运行时根据配置/输入切换具体实现。
- 需要通过基类容器存放异构对象并统一调度。
- 团队对模板元编程不熟,维护成本高于收益。
与虚函数多态的对比¶
- 分发时机:
- 虚函数是运行时分发(需 vtable),可通过基类指针/引用统一处理异构对象。
- CRTP 是编译时分发,要求在编译期就知道具体类型,换来零开销。
- 扩展方式:
- 虚函数通过覆盖虚方法扩展行为。
- CRTP 通过“静态接口”约束派生类提供实现,基类用
static_cast<Derived*> 调用。
- 适用场景:
- 运行时需要真正的动态绑定 → 选虚函数。
- 性能敏感、类型在编译期已知、可模板化 → 选 CRTP。
FAQ¶
- 为什么叫“奇异递归”?
- 因为基类模板的实参“递归地”引用了派生类本身,看起来很“奇怪”,但这是合法且有用的模板技巧。
- 可以多层套 CRTP 吗?
- 和 Concepts 是什么关系?
- Concepts 负责“约束”,CRTP 负责“复用 + 分发”。两者可结合使用,提升错误信息友好度与可维护性。
CRTP 是 C++ 模板元编程中的重要模式,在性能敏感场景(如游戏开发、高频交易等)尤为常见。它在不引入虚函数开销的前提下提供多态式的扩展能力,适合用于“编译期就能决定行为”的问题。但需要权衡模板复杂度与可维护性,避免过度抽象。