C++CRTP奇异递归模板模式
CRTP:C++里那个“自己继承自己”的聪明 trick
你有没有写过这样的代码:想让基类能调用派生类的函数,又不想用虚函数——毕竟虚表开销、动态绑定、多态那套太重了?或者你试过 static_cast<Derived*>(this),结果编译器报错说 Derived 还没定义完?这时候,CRTP 就像一个提前约好的暗号,悄悄把类型信息塞进编译期。
CRTP 全名 Curiously Recurring Template Pattern,中文常叫“奇异递归模板模式”。名字听着玄乎,其实就干一件事:让基类以模板参数的形式,提前“认出”自己的派生类。它不靠运行时,不靠虚函数,纯靠编译器在实例化那一刻把类型关系捋得明明白白。
举个最典型的例子:实现一个带计数功能的类族。
template<typename Derived>
struct Counter {
static inline size_t count = 0;
Counter() { ++count; }
~Counter() { --count; }
static size_t get_count() { return count; }
};
struct Widget : Counter<Widget> { /* 没有额外代码 */ };
struct Gadget : Counter<Gadget> {};
注意看:Counter<Widget> 继承自 Counter<Widget>?不,是 Widget 继承自 Counter<Widget>。这里的 Derived 是 Widget 自己——它在定义时就把自己“递归”地传给了基类。编译器看到 Widget : Counter<Widget>,立刻知道 Counter<Widget> 的所有静态成员(比如 count)都是专属 Widget 的,和 Gadget 完全隔离。两个类的计数互不干扰,零运行时成本。
这比手动写 static size_t Widget::count 干净得多,也比用 std::map<std::type_info, size_t> 查表快得多——后者连 typeid 都要开销,而 CRTP 的计数变量在 .bss 段里安安静静躺着。
CRTP 真正发力的地方,是静态多态。想象你要写一组容器适配器,希望它们共享统一接口(比如 size()、empty()),但每个实现逻辑完全不同,又不想扛虚函数那一套:
template<typename Derived>
struct ContainerInterface {
size_t size() const { return static_cast<const Derived*>(this)->do_size(); }
bool empty() const { return static_cast<const Derived*>(this)->do_size() == 0; }
};
struct VectorLike : ContainerInterface<VectorLike> {
std::vector<int> data;
size_t do_size() const { return data.size(); }
};
struct StackLike : ContainerInterface<StackLike> {
std::stack<int> s;
size_t do_size() const { return s.size(); } // 注意:std::stack::size() 是 O(n),但这是实现细节
};
这里没有 virtual,没有指针跳转,只有 static_cast ——编译器在生成 VectorLike::size() 时,已经把 do_size() 的地址焊死了。调用链是:vec.size() → ContainerInterface<VectorLike>::size() → VectorLike::do_size()。三步内完成,内联友好,性能透明。
有人会问:为什么非得用 static_cast?不能直接 this->do_size() 吗?不行。因为 ContainerInterface 模板里,Derived 是一个不完全类型(incomplete type)——VectorLike 正在定义中,编译器还不知道它有 do_size()。所以必须靠 static_cast 显式告诉编译器:“信我,这个 this 指针指向的就是 Derived,它一定有这个函数”。
这也是 CRTP 的隐性契约:派生类必须提供基类所依赖的所有接口,且命名、签名严格匹配。少了 do_size()?编译失败。返回类型不对?编译失败。这种“编译期契约”,比运行时 dynamic_cast 失败再抛异常,早了整整一个世界线。
再往深一层:CRTP 常被用在表达式模板(expression templates)里,比如实现惰性求值的矩阵运算。MatrixExpr<AddOp, LHS, RHS> 继承自 MatrixExprBase<MatrixExpr<AddOp, LHS, RHS>>,让基类能统一调度子表达式的 eval(),而具体计算延迟到最终赋值才触发。这种层层嵌套的“自我引用”,没有 CRTP 根本玩不转。
不过得提醒一句:CRTP 不是银弹。它会让继承体系变“硬”——Counter<Widget> 和 Counter<Gadget> 是完全无关的两个类,没法放进同一容器(除非你再套一层类型擦除)。如果你真需要运行时多态,CRTP 就该退场,交给虚函数或 std::variant。
另外,别滥用。见过有人给每个小工具类都套 CRTP,结果模板实例化爆炸,编译时间翻倍。CRTP 的价值在于解决特定问题:需要编译期确定类型关系、避免虚函数开销、实现静态接口约束。不是为了炫技。
最后一个小技巧:现代 C++20 可以用 requires 加一层约束,让错误信息更友好:
template<typename Derived>
struct ContainerInterface {
static_assert(requires(const Derived& d) { d.do_size(); });
// …
};
这样,如果 Derived 忘了实现 do_size(),编译器会直接告诉你“类型不满足 ContainerInterface 要求”,而不是一长串 static_cast 失败的模板展开。
CRTP 就像厨房里那把厚背砍骨刀——不常亮出来,但切冻肉、剁大骨时,你立刻懂它为什么存在。它不讨好初学者,也不追求通用,但它在 C++ 编译期元编程的地基上,凿出了最结实的一道榫卯。


还没有评论,来说两句吧...