文档目录

一、解决什么问题?

问题:你有一个通用的算法骨架(比如"遍历 + 处理 + 汇总"),但其中的"处理"步骤在不同类型中不同。有两种选择:

  1. 虚函数:方便,但每次调用都要查 vtable,而且虚函数不能内联。
  2. 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 有什么缺点?”

  1. 代码膨胀:每实例化一次 Base<Derived>,编译器生成一份完整代码。100 个派生类 = 100 份代码。
  2. 不能放容器:vector<Base<DerivedA>*> 和 vector<Base<DerivedB>*> 是两种不同的容器类型,不能混用。
  3. 测试困难:CRTP 基类不能单独实例化和测试,必须通过派生类。