CRTP-模板编程初次尝试

在 C++ 语言中,CRTP(Curiously Recurring Template Pattern,好奇递归模板模式)是一种高级模板技巧,常用于实现静态多态。它在需要避免虚函数开销、提高接口复用性、增强类型安全时具有显著优势。

在读代码的时候,有时会看到下面的写法:

1
class Derived : public Base<Derived>

之前我都是直接跳过的,但是最近写图数据结构的时候再次接触了,就把这部分详细看了看。

基本了解

1. CRTP 的基本形式

CRTP 的核心思想是:一个类将自身作为模板参数传递给其基类,这种设计模式可用于在编译期实现多态调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};

class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "Derived implementation\n";
}
};

调用 interface() 时,实际会在编译期解析并绑定到 Derived::implementation(),实现零运行时开销的多态。

2. 为什么使用 CRTP

优势

  • 零运行时开销:无虚表,无 RTTI,调用开销为常规函数级别;
  • 更容易内联:调用目标在编译期已知,编译器更容易进行内联优化;
  • 良好的可组合性:可以构造策略模板、表达式模板等结构,组合多个行为而不依赖多重继承。

3. CRTP vs 虚函数:性能对比

我们使用 Google Benchmark 测试三种函数调用方式:

  • BM_none:普通函数调用;
  • BM_dynamic:虚函数调用;
  • BM_crtp:CRTP 调用。

测试结果如下:

1
2
3
4
5
Benchmark           Time             CPU   Iterations UserCounters...
------------------------------------------------------------
BM_none 0.492 ns 0.484 ns 1000000000 items_per_second=66.0645G/s
BM_dynamic 36.2 ns 33.7 ns 21333333 items_per_second=949.797M/s
BM_crtp 0.510 ns 0.516 ns 1000000000 items_per_second=62.0606G/s

解读

  • 虚函数调用开销远高于 CRTP(约 65~70 倍);
  • CRTP 与普通函数调用几乎持平,是性能敏感场景下的优选。

适用场景

4. CRTP 的适用边界

不能替代虚函数的情况:

  • 无法支持运行时多态;
  • 无法使用 std::vector<Base*> 存储派生类对象;
  • 模板展开可能导致编译慢、代码膨胀;
  • 基类中不能依赖派生类的不完整定义(如 sizeof(Derived))。

CRTP的一个主要限制是基类B的大小不依赖他的模板参数T。B通过D进行实例化,无法获取D中完整的类型。

1
2
3
4
5
template<typename Derived>
class Base {
char buf[sizeof(Derived)]; // ❌ 错误,因为此时 Derived 是 incomplete
Derived member; // ❌ 错误,类大小依赖 T
};
1
2
Derived* ptr;  // OK
static_cast<Derived*>(this)->doSomething();

在 CRTP 模式中,基类 Base 的实例化时机早于 T 本身的定义完成。因此,Base不能依赖 T 的完整定义,比如大小、成员变量等。

适用场景:

  • 类型在编译期已知;
  • 不需要类型擦除或容器存储;
  • 性能关键路径(如表达式模板、驱动、渲染器等)。

5. 为什么 CRTP 没有广泛替代虚函数?

  • 不能实现运行时类型擦除:CRTP 类型不是多态接口,不能统一处理多个子类;
  • 写法复杂抽象Base<Derived> 的递归结构初学者难以理解;
  • 模板代码编译慢、可读性差
  • 调试器和 IDE 对模板支持不如虚函数好
  • 无法 override 检查,接口易失误

6. CRTP 与 Concepts 结合

C++20 引入的 Concepts 可为 CRTP 提供编译期约束:

1
2
3
4
5
6
7
8
9
template<typename T>
concept Shape = requires(const T& t) {
{ t.area() } -> std::convertible_to<double>;
};

template<Shape T>
void draw(const T& shape) {
std::cout << shape.area();
}
  • 增强类型安全;
  • 报错信息更友好;
  • 接口行为清晰、可组合。

相似替代

7. 模拟虚函数:LLVM 的 classof 技巧

LLVM 不用 dynamic_cast,也极少用 virtual。它大量使用 CRTP + 自定义 RTTI(运行时类型识别)来做类型擦除和动态分派。

LLVM 避免使用虚函数,改用自定义类型标记与静态分发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
public:
enum Kind { RECTANGLE, CIRCLE };
Base(Kind k) : kind(k) {}
Kind getKind() const { return kind; }
protected:
Kind kind;
};

class Rectangle : public Base {
public:
Rectangle() : Base(RECTANGLE) {}
static bool classof(const Base* b) {
return b->getKind() == RECTANGLE;
}
};

配合 isa<>cast<>dyn_cast<> 等工具模板,实现无虚表的运行时类型识别。

  • 零虚表
  • 自定义 RTTI
  • 类型检查精细可控
  • 用在 LLVM 全代码中(如 Instruction, Value, Type)

8. 手动 vtable:Boost.Dyno 风格类型擦除

想要 完全去除 virtual + 不用模板展开太多代码,还需要支持 runtime polymorphism。怎么办?

→ 模拟一个自己的 vtable

手动 vtable 核心思路

  1. 创建一个纯数据结构的“vtable”结构体(函数指针);
  2. 存储对象指针 + vtable 指针;
  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
struct ShapeVTable {
double (*area)(const void*);
};

template<typename T>
constexpr ShapeVTable make_vtable() {
return {
[](const void* ptr) -> double {
return static_cast<const T*>(ptr)->area();
}
};
}

class AnyShape {
public:
template<typename T>
AnyShape(T t) : ptr(new T(std::move(t))), vtbl(&make_vtable<T>()) {}
~AnyShape() { delete static_cast<const void*>(ptr); }

double area() const { return vtbl->area(ptr); }

private:
void* ptr;
const ShapeVTable* vtbl;
};

优点:

  • 手动控制开销(自定义内联/分支)
  • 类型擦除支持:运行时决定行为
  • 比 virtual 更灵活:可以控制是否复制、是否移动

这种方式用于标准库(如 std::function)、Boost.Dyno 或高性能框架中,提供更细粒度的控制。

总结

技术方案 是否运行时可用 是否类型擦除 使用虚函数 编译期开销 应用示例
CRTP ❌ 否 ❌ 否 STL 模板、表达式模板、数值计算
LLVM classof 技巧 ✅ 是 ✅ 是(自定义) 中等 LLVM IR 类型系统、Clang AST
手动 vtable(如 Boost.Dyno) ✅ 是 ✅ 是 中等 类型擦除库、自定义插件框架
虚函数 ✅ 是 ✅ 是 普通 OOP、Qt、标准库多态接口
特性 虚函数 CRTP
调用成本 较高(vtable) 极低(编译期绑定)
类型擦除 ✅ 支持 ❌ 不支持
容器支持 ✅ 多态容器 ❌ 需 variant
类型检查 运行时检查 编译期检查 + Concepts
报错可读性 一般 模板报错复杂但可控
代码维护性 易于理解 复杂,需经验

适用 CRTP:

  • 类型在编译期已知;
  • 性能要求高(无虚表开销);
  • 不需运行时类型擦除;
  • 接口可用模板静态组合。

适用虚函数:

  • 类型异构集合;
  • 运行时多态;
  • 调试友好、可维护性高。

CRTP 并不是虚函数的替代,而是服务于不同设计目标的技术工具。


CRTP-模板编程初次尝试
http://example.com/2025/06/29/CRTP/
作者
icyyoung
发布于
2025年6月29日
许可协议