crtp是唯一能在零虚函数、零对象尺寸开销、零调用速度损失下实现基类调用派生类逻辑的方案,其本质是编译期契约,依赖base显式特化使基类获知派生类具体类型,static_cast(this)实现编译期绑定,拒绝dynamic_cast等运行时机制。

CRTP 是唯一能让你在不引入虚函数、不增加对象尺寸、不牺牲调用速度的前提下,实现“基类调用派生类逻辑”的方案。它不是语法糖,而是编译期契约——一旦写错模板参数或漏掉 static_cast,编译器立刻报错,且错误信息往往指向派生类定义处,而非调用点。
为什么 Base<derived></derived> 不能写成 Base 或 Base<t></t>?
CRTP 的模板参数必须是**最终派生类类型本身**,这是整个机制成立的前提:
-
Base(非模板)无法感知派生类,static_cast<t>(this)</t>中的T就无从谈起 -
Base<t></t>(泛型模板参数)在基类内部仍无法确定具体类型,static_cast<t>(this)</t>会因T未特化而编译失败 - 只有
Base<derived></derived>这种显式特化,才能让基类模板实例化为“知道Derived有impl()成员”的具体类型
常见错误:把 class Derived : public Base<derived></derived> 写成 class Derived : public Base<void></void> 或 class Derived : public Base<base>> —— 编译器会报 invalid static_cast 或 incomplete type,根源是模板参数没闭环。
static_cast<derived>(this)</derived> 为什么不用 dynamic_cast?
因为 CRTP 的多态绑定发生在编译期,Derived 类型完全已知,无需运行时类型检查:
-
dynamic_cast要求基类带虚函数,直接违背“零虚函数”目标 -
dynamic_cast在指针为空或类型不匹配时返回nullptr,引入运行时分支和不确定性 -
static_cast是纯编译期操作,生成的汇编就是直接取地址 + 偏移,无任何条件跳转
注意:若误将 static_cast<derived>(this)</derived> 用于非 Derived 实例(如多层继承中传错模板参数),行为未定义,但编译器通常会在构造函数或成员函数调用前就捕获到类型不匹配。
CRTP 对象布局和 ABI 兼容性有哪些隐性限制?
CRTP 类型不具备跨模块多态能力,这是它和虚函数最根本的差异:
- 每个
Base<a></a>和Base<b></b>是完全不同的类型,无法共用同一指针类型(比如没有公共基类可容纳两者) - 不能像
VirtualBase*那样把Base<a>*</a>和Base<b>*</b>存进同一容器,除非你用std::variant<base>*, Base<b>*></b>或类型擦除包装 - 导出到 DLL / SO 时,
Base<derived></derived>的符号名含完整模板签名,不同编译单元若模板参数拼写稍异(如命名空间省略),会导致 ODR 违规
典型陷阱:在头文件里定义 template<typename d> class Base</typename>,但在多个 .cpp 中分别实例化 Base<myclass></myclass> —— 若 MyClass 定义有细微差异(如友元声明顺序),链接时可能静默失败或崩溃。
如何安全地在 CRTP 中访问派生类数据成员?
基类中通过 static_cast<derived>(this)</derived> 访问派生类成员是合法的,但需满足两个前提:
- 派生类必须已完全定义(不能是前置声明),否则
sizeof(Derived)未知,static_cast可能出错 - 访问的数据成员不能是 private(除非基类被声明为 friend),否则编译报
private member access
推荐做法:在派生类中将关键数据设为 protected,或提供 protected 的访问器函数。例如:
template <typename d>
class CounterBase {
public:
void inc() { static_cast<d>(this)->count_++; }
int get() const { return static_cast<const d>(this)->count_; }
};
class MyCounter : public CounterBase<mycounter> {
protected:
int count_ = 0; // 不是 private,供基类直接访问
};
</mycounter></const></d></typename>
若坚持用 private,必须加 friend class CounterBase<mycounter>;</mycounter>,但这样会破坏封装意图,且每个派生类都要重复写。
真正容易被忽略的是:CRTP 的“静态”本质意味着所有行为都锚定在模板参数上。一旦你试图在运行时切换策略(比如根据配置选 A 或 B 行为),CRTP 就不再适用——那该换 std::function 或策略类了。它解决的是“编译期已知的、固定的一组变体”,不是“运行时任意扩展的插件系统”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











