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 并不是虚函数的替代,而是服务于不同设计目标的技术工具。