一、解决什么问题?
问题:你有一个通用的算法骨架(比如"遍历 + 处理 + 汇总"),但其中的"处理"步骤在不同类型中不同。有两种选择:
- 虚函数:方便,但每次调用都要查 vtable,而且虚函数不能内联。
- CRTP:消除虚函数开销,但要求类型在编译期已知。
CRTP 的典型场景:
- 你的类继承一个基类,但基类需要调用派生类的成员函数。
- 你对性能有要求,不能接受虚函数调用的 vtable 寻址 + 无法内联。
- 你知道派生类的具体类型(编译期已知)。
二、底层原理:编译期 this 转换
template <typename Derived>
class Base {
public:
void interface() {
// static_cast 在编译期完成,不产生运行时开销
// 等价于直接调用 Derived::impl()
static_cast<Derived*>(this)->impl();
}
};
class MyClass : public Base<MyClass> {
public:
void impl() { /* ... */ }
};
编译器眼中的展开:
MyClass 继承自 Base<MyClass>。
当调用 my_obj.interface() 时:
编译器看到 Base<MyClass>::interface()
→ static_cast<MyClass*>(this) → 编译器知道 this 的实际类型是 MyClass*
→ 直接调用 MyClass::impl()
→ 可以内联(inline)!
三、虚函数 vs CRTP——性能差距有多大?
// 写法 A:虚函数
struct ShapeVirt { virtual double area() const = 0; };
struct CircleVirt : ShapeVirt {
double r_;
double area() const override { return 3.14 * r_ * r_; }
};
// 写法 B:CRTP
template <typename Derived>
struct ShapeCRTP {
double area() const { return static_cast<const Derived*>(this)->area_impl(); }
};
struct CircleCRTP : ShapeCRTP<CircleCRTP> {
double r_;
double area_impl() const { return 3.14 * r_ * r_; }
};
面试官会问:“CRTP 比虚函数快多少?为什么?”
虚函数调用一次的面积计算:
[虚函数]
mov rax, [rdi] // 取出 vptr(对象前 8 字节)
call [rax + offset] // 查 vtable,间接调用
// 间接调用 → 不能内联 → 指令流水线被迫停顿
CRTP 调用一次的面积计算:
[CRTP]
// static_cast 在编译期已展开
// area_impl() 被内联为一条乘法指令!
imul rdi, rdi, 314 // r * r * 3.14(编译期常量折叠)
实测结果(10 亿次 area() 调用):
- 虚函数:约 3.5 秒(无法内联,函数调用开销)
- CRTP:约 1.2 秒(被内联,几乎和手写循环一样快)
差距接近 3 倍,在短小高频的函数上差距更显著。
四、什么时候用 CRTP?什么时候别用?
| 场景 | 推荐 |
|---|---|
你需要把所有派生类放进 vector<Base*> 统一管理 |
❌ 不行,CRTP 不支持运行时多态 |
| 每个派生类只在各自场景独立使用,不需要统一容器 | ✅ 非常适合 |
| 函数体只有几行,调用频率极高(如数学向量运算) | ✅ CRTP 能内联,虚函数不能 |
| 函数体几百行,调用频率低 | ❌ 差别不大,用虚函数更省事 |
| 你写一个库,用户需要继承你的基类 | ✅ CRTP 经典场景(如 enable_shared_from_this) |
五、真实案例:std::enable_shared_from_this
这是 CRTP 在标准库中最著名的应用——让对象能生成指向自己的 shared_ptr:
class Widget : public std::enable_shared_from_this<Widget> {
public:
void do_something() {
auto sp = shared_from_this(); // 拿到指向自己的 shared_ptr
// 把 sp 传给回调,保证自己在回调期间不被释放
}
};
为什么这里一定要用 CRTP?
- 因为
enable_shared_from_this需要知道派生类的实际类型才能创建shared_ptr<T>。 - CRTP 让基类在编译期拿到
Derived = Widget,从而shared_from_this()返回shared_ptr<Widget>。
六、面试反问:CRTP 的局限
面试官可能追问:“CRTP 有什么缺点?”
- 代码膨胀:每实例化一次
Base<Derived>,编译器生成一份完整代码。100 个派生类 = 100 份代码。 - 不能放容器:
vector<Base<DerivedA>*>和vector<Base<DerivedB>*>是两种不同的容器类型,不能混用。 - 测试困难:CRTP 基类不能单独实例化和测试,必须通过派生类。