Appearance
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 尺寸)在编译期展开,无虚函数跳转、易向量化。虚函数放到每像素/每元素调用是很差的。
三、经典用途
- 统一接口 + 编译期定制(Policy/库里常见)。
- 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。
五、常见面试题
Base<Derived>会让 Derived 继承 Base<Derived> 导致递归占用过度吗? 不会——每个实例化 Base<X> 只存在一次,Derived:public Base<Derived>是合法的“两阶段特化引用”,不会无限实例化(因为没在语义上“用自己定义自己”)。CRTP 里
static_cast<Derived*>(this)为什么安全? 因为模板实例化后 this 实际确实是Derived*(Base 作为被 Derived 继承存在),是“已知派生”的静态向下转型,无虚表/多态风险(前提不把 Base<Derived> 基类转成别的)。静态多态 vs 动态多态该怎么选? 类型编译期已知、追求零开销 → CRTP/模板;需要运行期类型集合/插件 → 虚函数接口。
CRTP 常用于哪些库?——
std::enable_shared_from_this<T>本身即 CRTP(继承自 enable_shared_from_this 且其 API 需 T);range-v3、Eigen 大量在用(表达式模板其实也是广义静态多态)。
六、一句话记忆
CRTP = 派生类把自己类型交给基类,基类借此在编译期“知道”子类并零开销回调——把动态分派换成编译器内联的静态调用。