CRTP通过编译期绑定调用路径实现零开销接口模拟,基类模板直接static_cast调用派生类方法,无虚函数、无vtable;配合static_assert可约束接口契约,支持静态访问控制与编译期能力探测,但不适用于运行时多态场景。

CRTP 如何实现零开销的接口模拟?
CRTP 的核心价值不是“替代虚函数”,而是让编译器在编译期就确定调用路径,彻底消除 vtable 查找和间接跳转。典型做法是基类模板中直接调用 static_cast,而非声明 virtual 函数。
- 基类不带虚析构函数,也不含任何虚函数 —— 这是零开销的前提;若需安全 delete 派生对象,必须显式提供非虚析构 + 静态断言约束
- 派生类必须继承自
Base,且不能中途再被其他模板包装(否则static_cast会失败) - 常见误用:在基类中调用未在派生类定义的函数,编译错误信息往往冗长,建议配合
static_assert检查接口契约,例如:static_assert(std::is_same_v
().do_work()), void>, void>, "Derived must implement do_work()");
如何用 CRTP 实现静态访问控制(如 only-own-type 操作)?
利用模板参数 Derived 的唯一性,可在基类中限制某些操作仅对“本派生类自身”有效,比如禁止跨类型赋值、只允许同类型比较。
- 在基类中定义
operator==时,参数类型写死为const Derived&,而非泛化的const Base&—— 这样A{} == B{}直接不匹配重载,无需运行时判断 - 配合
friend和私有构造,可实现“仅本类型可构造”的工厂模式变体,例如:template
class NonCopyable { protected: NonCopyable() = default; ~NonCopyable() = default; public: NonCopyable(const NonCopyable&) = delete; NonCopyable& operator=(const NonCopyable&) = delete; friend T; // only T can access protected ctor/dtor }; - 注意:C++20 起可改用
requires std::same_as替代部分static_cast断言,但 CRTP 主体逻辑仍需保持模板参数显式传递
CRTP 与 SFINAE / Concepts 结合做编译期能力探测
CRTP 基类可以成为“能力分发中心”:根据派生类是否提供某个嵌套类型、成员函数或 constexpr 值,启用不同行为分支。
- 典型场景:统一序列化接口,但对支持
to_json()的类型走 JSON 路径,对支持serialize(Writer&)的走二进制路径 —— 所有决策在编译期完成 - 推荐写法:用
decltype+void_t或 C++20requires构建 trait,再通过if constexpr分支,避免宏或特化污染 - 陷阱:不要在基类模板中直接依赖派生类未实例化的成员(如未定义的
static constexpr),否则会导致 ODR-violation 或隐式实例化失败;应始终用 SFINAE 友好方式探测
为什么 CRTP 不适合替代所有多态场景?
CRTP 是静态多态,它无法处理运行时才确定类型的集合,比如 std::vector<:unique_ptr>> 这种异构容器。
立即学习“C++免费学习笔记(深入)”;
- 若强行用
std::vector<:unique_ptr>>>,每个Concrete对应独立的基类特化,无法共用同一容器类型 - 混合使用 CRTP 和动态多态(如基类含虚析构+CRTP 辅助功能)极易引发对象切片或
static_cast失败,除非严格约定内存布局一致且无虚函数干扰 - 调试困难:编译错误常出现在实例化深度较深的模板栈中,IDE 很难跳转到真正缺失的派生类成员定义处
真正需要运行时类型擦除的地方,别硬套 CRTP;它最锋利的用途,是把“本该在运行时做的类型判断”,压缩进编译期的一个模板参数里 —— 这个参数一旦写错,整个链条就断了,没有妥协余地。










