CRTP是编译期静态多态范式,要求派生类必须显式作为基类模板参数传入,否则编译失败;基类通过static_cast调用派生类成员,函数签名须严格一致,不支持运行时多态或类型擦除。

CRTP 不是“替代虚函数的备选方案”,而是彻底放弃运行时分派前提下的新设计范式——它要求你把类型决策前移到模板实例化阶段,否则编译直接失败。
CRTP 基类必须显式接收派生类类型作为模板参数
这是整个模式的起点,也是最容易漏掉的硬性约束。基类不是普通基类,而是模板类;派生类继承时必须把自身类型原样传进去,不能省略、不能用 auto、不能靠推导。
-
class Derived : public Base<derived></derived>✅ 正确:类型明确、可静态转换 -
class Derived : public Base❌ 编译错误:Base不是类型,是模板名 -
class Derived : public Base<void></void>❌ 即使能编译,static_cast<void>(this)</void>无法调用成员函数
典型错误现象是编译器报 invalid static_cast 或 “no member named 'xxx' in 'Base
派生类实现函数必须与基类中 static_cast 调用签名严格一致
CRTP 的调用链是纯静态的:Base<t>::interface()</t> → static_cast<t>(this)->implementation()</t>。中间没有重载解析、没有隐式转换、不走 ADL。
立即学习“C++免费学习笔记(深入)”;
- 函数名、参数个数、参数类型(含 const/volatile 限定)、返回值类型必须完全匹配
- 如果基类写的是
void draw() const,派生类写void draw()(非 const)→ 编译失败 - 如果基类调用
derived->compute(x, y),而派生类定义的是int compute(double, double)→ 匹配失败,不是隐式转换,是根本找不到函数
这种严格性带来好处:错误在编译期暴露;坏处:改一个签名就得同步动两边,容易漏。
CRTP 类型不兼容,不能用裸指针/引用做统一容器
Base<circle>*</circle> 和 Base<square>*</square> 是两个完全无关的类型,不能放进同一 std::vector,也不能用 Base>* 统一指向——C++ 没有“模板通配符”。
- 想存多种 CRTP 派生对象?得用类型擦除,比如
std::variant<circle square></circle>+std::visit,或包装成std::any - 想模拟多态调度?只能靠外部策略,例如用
enum ShapeType+switch分发,或用if constexpr在模板函数内分支 - 别试图写
Base<auto>*</auto>或template<typename t> using BasePtr = Base<t>*</t></typename>来“泛化指针”——语法不合法
这是 CRTP 最常被低估的限制:它解决的是单类型高性能调用,不是运行时异构集合管理。
模板膨胀与编译依赖需主动控制
每个 Base<t></t> 实例都会生成一份独立代码,Base<circle></circle> 和 Base<square></square> 的 interface() 函数体完全复制两份,不共享。
- 接口函数体大、且被高频调用?考虑把共用逻辑抽到 non-template 工具函数里,再由 CRTP 成员函数调用
- 基类头文件被大量包含?用 PIMPL 或模块(
export module)隔离实现细节,避免模板定义污染全局编译单元 - 用
concepts约束Derived必须满足的接口,比裸static_cast带来的错误信息更清晰,例如:requires std::is_invocable_v<decltype derived></decltype>
真正难的不是写对第一个 CRTP,而是当项目里出现第十个 Base<x></x> 时,还能一眼看出哪些行为是复用的、哪些是重复的、哪些本该用虚函数收口。


















