Skip to content

CRTP(奇异递归模板)与静态多态 / Mixin

一、CRTP 是什么

基于模板的复用技巧:派生类把自己作为基类的模板参数:

cpp
template <typename Derived>
class Base {
public:
    // 把 *this 静态转为 Derived* 再调其方法 —— 编译期多态
    void interface() { static_cast<Derived*>(this)->impl(); }
};
class Foo : public Base<Foo> {   // Derived = Foo
public:
    void impl() { /* Foo 专属实现 */ }
};

由于 Base<Foo>Base<Bar> 是不同基类,Base<Foo>::interface 调用时静态就知道是 Foo::impl——无需 vtable。

二、比虚函数(动态多态)好在哪 / 代价

虚函数多态CRTP 静态多态
分派运行期查 vtable编译期静态转型
开销指针间接跳转、难内联零开销,可内联(常被内联掉)
灵活性运行时才知道类型(容器放不同派生)编译期必须确定具体类型
使用多态运行时/接口订阅编译期算法统一、类型已知场景

HPC/AI 内核为什么爱用?——算子不同实现(如 float/double、行主/列主、tile 尺寸)在编译期展开,无虚函数跳转、易向量化。虚函数放到每像素/每元素调用是很差的。

三、经典用途

  1. 统一接口 + 编译期定制(Policy/库里常见)。
  2. Mixin 给类型加能力(比较符、运算符自动生成)。

Mixin:用继承“注入”公共能力

cpp
// 给任意 T 添加 operator==(比较其数据)
template <typename Derived> struct EqualityComparable {
    friend bool operator==(const Derived& a, const Derived& b){
        return a.v_ == b.v_;   // 假设 v_ 存在——CRTP + friend
    }
};
struct Point : EqualityComparable<Point> { int v_; };

(friend 模板内定义,ADL 找得到。这是「CRTP 生成运算符」范式。)

四、与 policy / policy-based 结合(Modern C++ Design)

HPC 里常把性能策略做成模板参:不是「继承子类 override」,而是「传 policy 模板参」:

cpp
template <typename Scalar, size_t BLK, typename Storage = RowMajor>
class GemmKernel {
    void run(...) { /* 用 Compile-time BLK 展开循环 */ }
};

这就是 policy-based design 的现代版本 + 模板推演出针对配置的专用 kernel。

五、常见面试题

  1. Base<Derived> 会让 Derived 继承 Base<Derived> 导致递归占用过度吗? 不会——每个实例化 Base<X> 只存在一次,Derived:public Base<Derived> 是合法的“两阶段特化引用”,不会无限实例化(因为没在语义上“用自己定义自己”)。

  2. CRTP 里 static_cast<Derived*>(this) 为什么安全? 因为模板实例化后 this 实际确实是 Derived*(Base 作为被 Derived 继承存在),是“已知派生”的静态向下转型,无虚表/多态风险(前提不把 Base<Derived> 基类转成别的)。

  3. 静态多态 vs 动态多态该怎么选? 类型编译期已知、追求零开销 → CRTP/模板;需要运行期类型集合/插件 → 虚函数接口。

  4. CRTP 常用于哪些库?—— std::enable_shared_from_this<T> 本身即 CRTP(继承自 enable_shared_from_this 且其 API 需 T);range-v3、Eigen 大量在用(表达式模板其实也是广义静态多态)。

六、一句话记忆

CRTP = 派生类把自己类型交给基类,基类借此在编译期“知道”子类并零开销回调——把动态分派换成编译器内联的静态调用。

C++ 面试八股 · VitePress 版