CRTP是唯一实现零开销基类调用派生类逻辑的方案,依赖编译期契约:模板实参不可省略、继承须public单继承、函数签名须字面一致、成员须public且定义可见。

CRTP 是唯一能在零虚函数、零对象尺寸开销、零调用速度损失下,让基类调用派生类逻辑的方案。它不是“能跑就行”的技巧,而是一套编译期契约:模板参数写错、函数签名差一个 const、继承方式不对,编译器立刻报错——但错误位置常在 static_cast<derived>(this)->func()</derived> 这一行,实际问题却藏在派生类定义里。
为什么 class Derived : public Base 一定编译失败
因为 Base 是模板名,不是类型;编译器根本不知道你要实例化哪个特化版本。它需要具体类型来生成可调用的代码,比如 Base<circle></circle> 或 Base<vec3>></vec3>。漏掉模板实参就等于没给编译器“钥匙”,它连门都打不开。
-
class Circle : public Base→ error: use of class template 'Base' requires template arguments -
class Circle : public Base<void></void>→static_cast<void>(this)</void>后无法调用任何成员,后续所有->draw_impl()都失败 -
class Vec3f : public Base<vec3></vec3>→ 错误:Vec3是模板,不是类型;必须写Base<vec3>></vec3> - 若派生类定义在基类模板之前(仅前向声明),会触发
incomplete type错误,因为Base<derived></derived>实例化时要读它的成员签名
static_cast<derived>(this)</derived> 调用失败的真正原因
这个转换本身不崩溃,但它合法的前提是:派生类必须以 public 方式单继承该 CRTP 基类,且不能有虚继承或多重继承干扰内存布局。一旦破坏这个前提,static_cast 就变成未定义行为——Clang/GCC 开 -Wall 会警告,但不拦着你编译通过。
- 私有/保护继承 →
static_cast编译失败:conversion from 'Base*' to 'Derived*' is inaccessible - 多重继承(如
class D : public Base<d>, public Other</d>)→this指针偏移错位,->func()可能读到其他基类的内存 - 构造函数体内调用
static_cast<derived>(this)->func()</derived>→ 危险!此时派生类成员尚未构造,哪怕函数体只读this地址也属未定义 - 绝不能用
dynamic_cast替代:报错cannot dynamic_cast 'this' because 'Base' is not polymorphic
如何让编译错误指向真正缺失的地方
默认情况下,错误信息卡在 static_cast<derived>(this)->draw_impl()</derived>,但真正问题是 Derived 根本没定义这个函数。靠 static_assert 提前拦截,把错误拉回基类定义处,避免排查绕弯。
立即学习“C++免费学习笔记(深入)”;
- 在基类接口函数里加校验:
static_assert(std::is_same_v<decltype>().draw_impl()), void>, "Derived must implement void draw_impl()");</decltype> - 用
std::is_detected_v自定义 trait 检查函数存在性与完整签名,比手动写decltype更健壮 - 所有被调用的函数必须是
public;如果派生类只在private区实现draw_impl(),static_cast后调用仍会失败 - 签名必须字面一致:
void draw_impl() const和void draw_impl()是两个函数;基类调非 const 版本,你就得提供非 const 版本
为什么你没法用 std::vector<base>> 存多种派生类
因为 Base<circle></circle> 和 Base<rect></rect> 是完全不同的类型,没有公共基类,也不满足同一模板参数。C++ 没有 Base> 这种通配语法,这不是限制,而是设计必然——CRTP 的代价就是放弃运行时类型擦除。
-
std::vector<:unique_ptr>></:unique_ptr>(带虚函数)→ 可行,但有虚表开销 -
std::vector<:variant rect triangle>></:variant>→ 零成本抽象,配合std::visit分发,推荐用于需混合操作的小规模类型集 -
std::vector<:any></:any>或自定义小对象封装 → 引入间接跳转和堆分配,失去 CRTP 的核心优势 - 真要兼顾性能和统一接口?外层加一层虚基类(如
class Drawable { virtual void draw() = 0; }),让 CRTP 派生类同时继承它,桥接静态与动态
CRTP 最容易被忽略的点不在语法,而在“可见性”和“时机”:所有相关代码必须在同一翻译单元可见,或通过显式模板实例化(template class Base<circle>;</circle>)导出符号;否则头文件里定义的基类模板,在 cpp 文件里用派生类实例化时,会因定义不可见而静默失效。


















