CRTP是编译期确定类型组合的架构选择,要求派生类显式传入自身类型作为基类模板参数,所有调用依赖static_cast和静态解析,不支持运行时切换,需用static_assert和if constexpr保障正确性。

CRTP 不是“替代虚函数的技巧”,而是彻底放弃运行时分发的架构选择——它要求你在编译期就决定类型组合,且无法在运行时切换行为。
CRTP 的基类必须显式接收派生类类型作为模板参数
这是整个模式成立的前提。基类不是普通基类,而是一个模板;派生类继承时,必须把自身类型原样传进去,否则 static_cast<derived>(this)</derived> 会编译失败。
- 错误写法:
class Derived : public Base {}—— 缺少模板实参,Base是不完整类型 - 正确写法:
class Derived : public Base<derived> {}</derived> - 若派生类带模板参数(如
class Vec3<t></t>),则需传入完整特化名:class Vec3f : public Base<vec3>> {}</vec3> - 基类中所有对派生类成员的调用,都依赖这个静态转换,因此派生类必须提供对应签名的公有成员函数
派生类方法必须与基类调用点严格匹配,否则 SFINAE 不救你
CRTP 没有虚函数表兜底,调用链完全靠编译器静态解析。一旦派生类漏实现、拼错名、参数类型不一致或 const 限定不匹配,错误信息往往指向基类中的 static_cast 行,而非缺失定义的位置。
- 常见报错:
error: invalid static_cast from type 'Base<derived>*' to type 'Derived*'</derived>—— 实际原因是Derived未定义被调用的函数 - 函数签名差异(比如基类调
draw(),派生类写Draw()或draw() const)会导致链接期“undefined reference”或更隐蔽的模板实例化失败 - 建议在基类接口中加
static_assert检查关键成员是否存在,例如:static_assert(std::is_same_v<decltype int>, "Derived must implement getValue()");</decltype>
CRTP 与 if constexpr 配合才能处理分支逻辑
纯 CRTP 基类无法根据派生类特性做条件分支——它不知道派生类有没有某个嵌套类型或 constexpr 值。这时候必须靠 if constexpr 在模板函数内部做编译期裁剪。
立即学习“C++免费学习笔记(深入)”;
- 错误思路:在基类构造函数里写
if (Derived::has_optimization)—— 这是运行时判断,且Derived::has_optimization可能根本不可见 - 正确做法:把分支逻辑封装进模板成员函数,用
if constexpr检查表达式有效性,例如:if constexpr (has_member_v<derived value_type>) { /* 使用 Derived::value_type */ }</derived> - 需要配合
std::is_detected_v或自定义 trait 判断派生类是否提供某类型/函数,否则if constexpr会因硬错误直接终止编译
CRTP 实例化爆炸和 ODR 违规风险真实存在
每个派生类都会实例化一份独立的基类模板副本,看似零开销,但当基类逻辑复杂、包含大量内联函数或静态数据时,代码体积和编译时间会显著上升。更危险的是跨 TU(translation unit)的 ODR 问题。
- 若两个源文件各自定义了
class A : public Base<a> {}</a>和class B : public Base<b> {}</b>,而Base中有 inline 函数访问了同一份静态局部变量,则不同 TU 中该变量可能有多份副本,违反 ODR - 避免方式:将基类中所有非模板逻辑抽到非模板辅助类中,或确保所有 inline 函数不含静态局部状态
- 调试时注意:gdb 显示的函数名类似
Base<a>::interface()</a>和Base<b>::interface()</b>是两个完全不同的符号,不能混用
真正难的不是写出第一个 CRTP 示例,而是守住“编译期确定”这条线——任何试图在运行时改变派生类身份、或通过 void* / union / reinterpret_cast 绕过类型系统的行为,都会让整个设计坍塌成不可维护的幻觉。


















