crtp核心写法是派生类以自身为模板参数继承基类,如class derived : public base;关键在于基类模板实例化时获知派生类类型,通过static_cast(this)调用派生类成员。

CRTP 的核心写法长什么样?
CRTP(Curiously Recurring Template Pattern)本质就是派生类把自身作为模板参数传给基类,让基类在编译期“知道”派生类类型。关键不是继承关系,而是 Base<derived></derived> 这个模板实例化动作。
常见错误是漏掉模板参数或写错继承顺序:
- 写成
class Derived : public Base(没传模板参数)→ 编译失败,Base不是具体类型 - 写成
class Derived : public Base<derived></derived>,但Base没声明为模板 → 同样编译失败 - 在
Base里调用static_cast<derived>(this)->foo()</derived>,但foo()在Derived中未定义 → 链接前就报错,错误信息通常是 “no member named 'foo' in 'Derived'”
什么时候该用 CRTP 替代虚函数?
当你确定多态行为在编译期完全可知、且不需运行时切换实现时,CRTP 才真正零开销。典型场景是容器适配器、数值计算策略、事件处理器注册等。
比如实现一个可插拔的序列化策略:
立即学习“C++免费学习笔记(深入)”;
template<typename Impl>
struct Serializer {
template<typename T>
void serialize(const T& t) {
static_cast<Impl*>(this)->do_serialize(t);
}
};
<p>struct JSONSerializer : Serializer<JSONSerializer> {
template<typename T>
void do_serialize(const T& t) { /<em> 实际 JSON 输出逻辑 </em>/ }
};对比虚函数方案:serialize() 调用无 vtable 查找、无指针间接跳转,内联也更友好。但如果需要运行时决定用 JSON 还是 XML 序列化器,CRTP 就不适用——它压根没提供运行时选择能力。
CRTP 带来的隐含约束和兼容性坑
CRTP 不是语法糖,它会改变类的继承图谱和模板实例化边界,容易引发意料外的 ODR(One Definition Rule)问题或 SFINAE 失败。
-
Derived必须完整定义后才能实例化Base<derived></derived>,所以不能在Derived声明(非定义)阶段就继承Base<derived></derived> - 两个不同 CRTP 基类(如
Loggable<t></t>和Serializable<t></t>)同时被继承时,若都依赖static_cast<t>(this)</t>,而T是最终派生类,一般没问题;但若中间有非 CRTP 层,static_cast可能越界 - 泛型算法如果依赖
std::is_base_of_v<base>, T>,结果是false——因为Base<t></t>和T是独立特化,不存在 is-a 关系;得改用std::is_convertible_v<t base>*></t>
CRTP 和普通模板继承的区别在哪?
区别在于“基类能否访问派生类的静态成员和函数”。普通模板继承(如 template<typename t> class Base {}</typename>)中,Base<t></t> 对 T 是黑盒;CRTP 则通过 static_cast<t>(this)</t> 强制打开这个盒子,代价是要求 T 必须是最终派生类,且所有调用点必须确保 this 指针实际指向 T 对象。
例如想让基类自动提供 name() 返回派生类名:
template<typename Derived>
struct Named {
const char* name() const {
return typeid(Derived).name(); // OK,Derived 是完整类型
}
};但如果写成 template<typename t> struct Base { const char* name() { return typeid(T).name(); } }</typename>,效果一样——但这就不是 CRTP,只是普通模板。CRTP 的价值不在 typeid,而在能调用 Derived::specific_method() 并参与内联优化。
真正容易被忽略的是:CRTP 基类里的所有函数都变成模板函数,哪怕只用一次,也会为每个派生类生成一份代码。如果基类逻辑复杂、又有很多派生类,编译时间和二进制体积可能明显上涨。










