crtp是c++中唯一零开销实现基类调用派生类逻辑的编译期技术,要求基类为模板且派生类显式传入自身类型,函数签名严格匹配,不支持运行时多态或异构容器。

CRTP 是目前 C++ 中唯一能在零虚函数、零对象尺寸开销、零调用速度损失下,让基类调用派生类逻辑的可行路径。它不是“避免虚函数的技巧”,而是彻底放弃运行时分发的编译期契约——写错模板参数、漏掉 static_cast、签名不一致,编译器当场报错,不会等到链接或运行时。
CRTP 基类必须显式接收派生类类型作为模板参数
这是整个模式成立的硬性前提,不是可选语法糖。基类必须是模板,且派生类继承时必须原样传入自身完整类型:-
class Circle : public ShapeBase<circle></circle>✅ 正确:编译器能推导出内存布局、成员存在性、调用合法性 -
class Circle : public ShapeBase❌ 编译失败:ShapeBase是模板名,非类型 -
class Circle : public ShapeBase<void></void>❌ 即使侥幸编译,static_cast<void>(this)</void>后无法访问任何成员 - 若派生类带模板参数(如
template<typename t> class Vec3</typename>),必须传入特化名:class Vec3f : public Base<vec3>></vec3>
error: invalid static_cast from type 'Base<derived>*' to type 'Derived*'</derived>,实际根源往往是模板实参没闭环,而非指针转换本身有问题。
派生类函数签名必须与基类 static_cast 调用点严格一致
CRTP 不做重载解析、不走 ADL、不隐式转换。基类中一句 static_cast<derived>(this)->draw()</derived>,要求 Derived 必须存在且仅存在一个公有成员函数:
- 函数名完全相同(
draw≠Draw≠draw_impl) - 参数个数、类型(含
const限定)、默认值全部匹配 - 返回类型需协变兼容;建议保持完全一致,避免歧义
- 若基类调用
void draw() const,派生类只提供void draw()(非 const)→ 编译失败
static_cast 行,而非派生类缺失定义处。建议在基类关键接口里加 static_assert 显式校验,例如:static_assert(std::is_same_v<decltype>().draw()), void>, "Derived must implement draw() returning void");</decltype>
用 if constexpr 实现编译期分支,别碰 typeid 或运行时判断
当不同派生类需要差异化逻辑时,不能靠运行时类型识别:
- ❌
if (typeid(*this) == typeid(Circle))—— 退化为运行时多态,且要求基类有虚函数 - ✅ 在模板成员函数内用
if constexpr判断类型特征:if constexpr (std::is_same_v<derived circle>) { ... }</derived> - 所有分支内的表达式都必须语法合法,哪怕未被选中;否则编译失败
- 对嵌套类型或成员函数的存在性判断,需配合
std::is_detected_v等 trait,不能直接写Derived::value_type
if constexpr 是 CRTP 分支能力的唯一安全出口,但它不解决类型异构容器问题——Base<circle></circle> 和 Base<square></square> 仍是两个完全无关类型,无法统一持有。
CRTP 类型无法统一存储,别试图用裸指针模拟多态容器
这是最容易被忽略的限制,也是和虚函数最根本的差异:-
std::vector<base>*> v;和std::vector<base>*> v;是两种不同类型,无法混存 -
Base<derived>*</derived>没有公共基类,不存在类似VirtualBase*的通用指针类型 - 想存多种 CRTP 派生对象?只能用
std::variant<circle square></circle>+std::visit,或包装成std::any/ 自定义小对象封装 - 模板实例化膨胀真实存在:
Base<circle></circle>和Base<square></square>的每个成员函数都会独立实例化,二进制体积可能显著增长
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











