CRTP-模板编程初次尝试
在 C++ 语言中,CRTP(Curiously Recurring Template Pattern,好奇递归模板模式)是一种高级模板技巧,常用于实现静态多态。它在需要避免虚函数开销、提高接口复用性、增强类型安全时具有显著优势。
在读代码的时候,有时会看到下面的写法:
1 |
|
之前我都是直接跳过的,但是最近写图数据结构的时候再次接触了,就把这部分详细看了看。
基本了解
1. CRTP 的基本形式
CRTP 的核心思想是:一个类将自身作为模板参数传递给其基类,这种设计模式可用于在编译期实现多态调用。
1 |
|
调用 interface()
时,实际会在编译期解析并绑定到 Derived::implementation()
,实现零运行时开销的多态。
2. 为什么使用 CRTP
优势
- 零运行时开销:无虚表,无 RTTI,调用开销为常规函数级别;
- 更容易内联:调用目标在编译期已知,编译器更容易进行内联优化;
- 良好的可组合性:可以构造策略模板、表达式模板等结构,组合多个行为而不依赖多重继承。
3. CRTP vs 虚函数:性能对比
我们使用 Google Benchmark 测试三种函数调用方式:
BM_none
:普通函数调用;BM_dynamic
:虚函数调用;BM_crtp
:CRTP 调用。
测试结果如下:
1 |
|
解读
- 虚函数调用开销远高于 CRTP(约 65~70 倍);
- CRTP 与普通函数调用几乎持平,是性能敏感场景下的优选。
适用场景
4. CRTP 的适用边界
不能替代虚函数的情况:
- 无法支持运行时多态;
- 无法使用
std::vector<Base*>
存储派生类对象; - 模板展开可能导致编译慢、代码膨胀;
- 基类中不能依赖派生类的不完整定义(如
sizeof(Derived)
)。
CRTP的一个主要限制是基类B的大小不依赖他的模板参数T。B通过D进行实例化,无法获取D中完整的类型。
1 |
|
1 |
|
在 CRTP 模式中,基类 Base
适用场景:
- 类型在编译期已知;
- 不需要类型擦除或容器存储;
- 性能关键路径(如表达式模板、驱动、渲染器等)。
5. 为什么 CRTP 没有广泛替代虚函数?
- 不能实现运行时类型擦除:CRTP 类型不是多态接口,不能统一处理多个子类;
- 写法复杂抽象:
Base<Derived>
的递归结构初学者难以理解; - 模板代码编译慢、可读性差;
- 调试器和 IDE 对模板支持不如虚函数好;
- 无法 override 检查,接口易失误。
6. CRTP 与 Concepts 结合
C++20 引入的 Concepts 可为 CRTP 提供编译期约束:
1 |
|
- 增强类型安全;
- 报错信息更友好;
- 接口行为清晰、可组合。
相似替代
7. 模拟虚函数:LLVM 的 classof 技巧
LLVM 不用 dynamic_cast
,也极少用 virtual
。它大量使用 CRTP + 自定义 RTTI(运行时类型识别)来做类型擦除和动态分派。
LLVM 避免使用虚函数,改用自定义类型标记与静态分发:
1 |
|
配合 isa<>
、cast<>
、dyn_cast<>
等工具模板,实现无虚表的运行时类型识别。
- 零虚表
- 自定义 RTTI
- 类型检查精细可控
- 用在 LLVM 全代码中(如 Instruction, Value, Type)
8. 手动 vtable:Boost.Dyno 风格类型擦除
想要 完全去除 virtual + 不用模板展开太多代码,还需要支持 runtime polymorphism。怎么办?
→ 模拟一个自己的 vtable
手动 vtable 核心思路:
- 创建一个纯数据结构的“vtable”结构体(函数指针);
- 存储对象指针 + vtable 指针;
- 所有操作通过函数指针调度。
1 |
|
优点:
- 手动控制开销(自定义内联/分支)
- 类型擦除支持:运行时决定行为
- 比 virtual 更灵活:可以控制是否复制、是否移动
这种方式用于标准库(如 std::function
)、Boost.Dyno 或高性能框架中,提供更细粒度的控制。
总结
技术方案 | 是否运行时可用 | 是否类型擦除 | 使用虚函数 | 编译期开销 | 应用示例 |
---|---|---|---|---|---|
CRTP | ❌ 否 | ❌ 否 | ❌ | 高 | STL 模板、表达式模板、数值计算 |
LLVM classof 技巧 | ✅ 是 | ✅ 是(自定义) | ❌ | 中等 | LLVM IR 类型系统、Clang AST |
手动 vtable(如 Boost.Dyno) | ✅ 是 | ✅ 是 | ❌ | 中等 | 类型擦除库、自定义插件框架 |
虚函数 | ✅ 是 | ✅ 是 | ✅ | 低 | 普通 OOP、Qt、标准库多态接口 |
特性 | 虚函数 | CRTP |
---|---|---|
调用成本 | 较高(vtable) | 极低(编译期绑定) |
类型擦除 | ✅ 支持 | ❌ 不支持 |
容器支持 | ✅ 多态容器 | ❌ 需 variant |
类型检查 | 运行时检查 | 编译期检查 + Concepts |
报错可读性 | 一般 | 模板报错复杂但可控 |
代码维护性 | 易于理解 | 复杂,需经验 |
适用 CRTP:
- 类型在编译期已知;
- 性能要求高(无虚表开销);
- 不需运行时类型擦除;
- 接口可用模板静态组合。
适用虚函数:
- 类型异构集合;
- 运行时多态;
- 调试友好、可维护性高。
CRTP 并不是虚函数的替代,而是服务于不同设计目标的技术工具。