CRTP是C++中唯一满足零虚函数、零对象尺寸开销、零调用间接跳转的编译期多态方案,但需严格遵循类型契约、公有继承、完整派生类类型及static_cast(this)强制转换等约束。

CRTP 是目前 C++ 中唯一能同时满足「零虚函数、零对象尺寸开销、零调用间接跳转」的编译期多态方案,但它不是“写对模板参数就能跑通”的语法糖,而是一套必须手动对齐类型契约、签名、实例化时机的静态绑定机制。
为什么 static_cast<derived>(this)</derived> 不是可选的,而是强制要求
基类模板本身不包含任何派生类成员的声明,interface() 函数里那句 static_cast<derived>(this)->impl()</derived> 是编译器能找到 impl() 的唯一路径。漏掉它,编译器连函数名都找不到——因为 Base 作用域里根本没定义 impl。
-
Derived必须是完整类型(不能是前置声明),否则static_cast无法校验内存布局兼容性 - 继承必须是公有的:
private或protected继承会导致static_cast编译失败 - 多重继承下若
Base<derived></derived>不是首基类,static_cast可能产生未定义行为;CRTP 通常应避免多重继承 - Clang/GCC 在
-Wall下会对非法static_cast给出明确提示,这是早期发现问题的关键信号
Derived 成员函数签名必须和基类调用点严格一致
CRTP 没有重载解析、没有隐式转换、不走 ADL。它只做字面匹配:函数名、参数个数、参数类型(含 const 限定)、返回类型,全部必须完全一致。
- 基类调
draw() const,派生类只提供draw()(非 const)→ 编译失败 - 基类调
compute(int, int),派生类定义compute(double, double)→ 找不到匹配函数,不是隐式转换问题 - 拼写错误(如
draw_imple())不会被宽容,错误信息常指向static_cast行,而非缺失定义处 - 建议在基类中加
static_assert校验关键成员存在性,例如:static_assert(std::is_same_v<decltype>().impl()), void>, "Derived must implement impl()")</decltype>
如何用 if constexpr 在 CRTP 中做编译期分支
当不同派生类需在统一接口中执行差异化逻辑时,运行时判断(如 typeid 或虚函数分发)直接退化为动态多态,违背 CRTP 初衷。正确做法是把分支封装进模板成员函数,用 if constexpr 对 Derived 类型做编译期裁剪。
立即学习“C++免费学习笔记(深入)”;
- 分支表达式必须在编译期可判定有效性,且所有分支中的符号仍需满足语法正确性
- 不能在未选中分支里调用仅在某派生类中定义的函数——编译器仍会尝试解析,导致硬错误
- 推荐配合
std::is_detected_v或自定义 trait 判断成员是否存在,例如:if constexpr (has_member_v<derived value_type>) { use(Derived::value_type{}); }</derived> - 重度行为分化建议改用策略模板(
template<typename policy></typename>),而非堆砌if constexpr
CRTP 导致的模板实例化膨胀怎么定位和收敛
每个 Base<a></a> 和 Base<b></b> 是完全独立的类型,其成员函数会被分别实例化。哪怕 interface() 实现一模一样,也会生成两套代码,增大二进制体积。
- 用
nm -C your_binary | grep Base或llvm-size -m定位膨胀源头 - 把计算密集、逻辑稳定的部分抽成独立
inline函数或constexpr工具类,让 CRTP 基类只做调度胶水 - 对行为高度一致的派生类,优先考虑策略模板复用同一份实例化代码
- 禁用无意义的显式模板实例化(如
template class Base<a>;</a>),除非你真需要导出符号
最常被忽略的点是:CRTP 解决的是单类型高性能调用,不是运行时异构集合管理。一旦你需要把不同派生类对象统一持有或调度,就必须跳出 CRTP 范式——要么加一层虚接口桥接,要么用 std::variant + std::visit,而不是强行塞进一个“通用基类指针”。


















