CRTP是编译期多态机制,非虚函数替代方案:要求派生类显式传入自身类型,否则编译失败;static_cast依赖该类型做静态检查;派生类须完整定义后才能实例化基类;不支持多态容器与多重继承。

CRTP 不是“替代虚函数的更高级写法”,而是彻底换了一条路:编译期确定类型、零间接跳转、不允许多态容器。用错场景会编译失败或运行时崩溃,不是语法问题,而是模型误用。
CRTP 基类必须显式传入派生类类型,漏写或传错就编译不过
基类 Base 本身是模板,Base 和 Base 是两个完全无关的类型。编译器靠这个模板参数在 static_cast 时做静态类型检查——没传、传成 void、传成中间基类(比如 Base 但实际继承的是 Final : public Middle),都会触发错误。
常见错误现象:
-
error: 'Derived' does not name a type—— 忘记在基类定义中声明模板参数template -
error: invalid static_cast from type 'Base—— 派生类写的是*' to type 'Concrete*' class D : public Base,而非class D : public Base - 运行时崩溃 ——
static_cast目标类型与对象内存布局不匹配(多重继承下尤其危险;CRTP 本就不鼓励多重继承)
派生类必须完整定义后才能实例化基类模板
Base 在模板实例化时,可能要访问 Derived::implementation() 的签名甚至函数体(比如该函数是 constexpr 或被内联调用)。所以不能在 Derived 仅声明(class Derived;)而未定义完成前,就让基类模板“看到”它。
典型约束:
- 头文件中定义
Base模板 →Derived定义必须放在该头文件之后,或同一翻译单元靠后位置 - 想把
Derived放进单独.cpp?不行。CRTP 是编译期绑定,所有相关代码需可见;要么把Derived也放进头文件,要么在.cpp中加显式实例化:template class Base; - 循环依赖?避免
Base和Derived互相包含定义;优先用前向声明 + 分离接口/实现,或改用函数模板重载替代部分 CRTP 场景
CRTP 无法存进统一容器,别试图用 std::vector >
Base 和 Base 是不同模板特化,类型不兼容,没有公共基类。你不能像 std::vector<:unique_ptr>> 那样持有多种派生对象并统一调用。
可行路径只有三条:
- 放弃统一容器,改用
std::variant+std::visit,保持零成本抽象同时支持有限多态 - 外层包一层虚接口(如
class Drawable { virtual void draw() = 0; };),让 CRTP 派生类同时继承该虚基类,桥接编译期逻辑与运行时调度 - 用模板参数驱动整个业务栈(如
template),把类型选择推到最外层入口,内部全程单态void run_sensor_loop();
CRTP 的性能优势只在调用链可内联时才真正生效
CRTP 的核心收益不是“去掉虚表”,而是让编译器能看清调用目标,从而执行函数内联、常量传播、死代码消除等优化。但如果 Base 被声明为 virtual、或跨翻译单元调用未开启 LTO、或函数体过大被编译器拒绝内联,那和普通函数调用没区别。
实操注意点:
- 确保
Base中转发函数(如interface())定义在头文件内,且不加virtual - 派生类的实现函数建议定义在头文件中(或标记
inline),否则链接时可能因 ODR 违规报错 - 嵌入式或实时系统中,用
g++ -O2 -fdump-tree-optimized检查关键路径是否真被内联;若没内联,CRTP 就只是徒增模板膨胀
CRTP 最容易被忽略的点,是它根本不是“多态”的替代品,而是“单态泛型”的强化工具——每个 Base 实例只服务一种 T,且这种绑定发生在模板实例化那一刻,不可更改。一旦需求里出现“运行时决定用哪个实现”,就得立刻切出 CRTP 范畴,引入虚函数或 std::variant。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











