crtp是派生类以自身为模板参数继承基类的编译期静态多态惯用法,解决虚函数调用开销与运行时类型擦除问题,通过static_cast(*this)实现零成本派生类成员访问。

CRTP 是什么,它解决什么问题
CRTP(Curiously Recurring Template Pattern)不是语言特性,而是一种惯用法:派生类以自身为模板参数继承基类。它让基类在编译期“知道”派生类的具体类型,从而实现静态多态——绕过虚函数调用开销,也避免运行时类型擦除。
典型结构是:template<typename derived> class Base</typename>,然后 class Derived : public Base<derived></derived>。关键在于基类中能通过 static_cast<derived>(*this)</derived> 安全调用派生类的成员函数,且整个过程零运行时成本。
如何正确写一个 CRTP 基类
基类必须是模板,并显式依赖 Derived 类型;派生类必须完整实例化该模板,不能只写 Base 或漏掉模板实参。
- 基类中访问派生类成员,一律用
static_cast<derived>(*this)</derived>,不要用dynamic_cast(CRTP 不涉及运行时类型信息) - 构造函数里不能调用派生类的非静态成员函数——此时派生类对象尚未构造完成,
static_cast虽语法合法,但行为未定义 - 如果基类需要默认构造,确保派生类也提供对应构造函数,否则
Derived()可能因基类构造失败而不可用 - 模板参数名(如
Derived)只是占位符,不强制要求和派生类同名,但语义清晰更重要
示例:
template <typename derived>
struct Countable {
static int count;
Countable() { ++count; }
~Countable() { --count; }
int id() const { return static_cast<const derived>(this)->m_id; }
};
<p>template <typename derived> int Countable<derived>::count = 0;</derived></typename></p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/gongju/2823" title="C++14"><img
src="https://img.php.cn/upload/manual/001/431/639/6ac8b33c327c4749.png" alt="C++14" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/gongju/2823" title="C++14" class="overflowclass">C++14</a>
<p class="overflowclass">C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。</p>
</div>
<a rel="nofollow" href="/xiazai/gongju/2823" title="C++14" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
<p>struct Widget : Countable<widget int m_id="next_id++;" private: static next_id widget::next_id="0;</p"></widget></p></const></typename>
CRTP 和普通继承、虚函数的关键区别
CRTP 是编译期绑定,虚函数是运行时绑定。这意味着:
-
sizeof(Widget)不会因继承Countable<widget></widget>而增加 vptr 字段,内存布局完全由派生类决定 - 编译器能看到所有调用路径,能内联
id()这类函数,而虚函数调用通常无法跨虚表优化 - 没有虚析构函数风险:CRTP 基类通常不设计成被裸指针 delete,若真要多态销毁,仍需加
virtual ~Base() = default,但这已偏离 CRTP 初衷 - 不支持向上转型后向下转型:你不能写
Base<widget><em> p = new Widget</em></widget>再安全转回Widget,因为Base<widget></widget>和Base<gadget></gadget>是完全不同类型
容易踩的坑:SFINAE、友元和模板推导失效
CRTP 常和模板元编程混用,这时几个隐性陷阱特别高发:
- 在基类里写
using Derived::foo可能触发 SFINAE 失败,应改用decltype(static_cast<derived>(*this).foo())</derived>配合std::declval做约束 - 派生类的私有成员无法被基类访问,即使用了
static_cast—— 必须显式声明friend class Base<derived></derived>,且 friend 声明必须出现在派生类内部 - 用
auto推导返回值时,若返回的是static_cast<derived>(*this)</derived>,实际类型是Derived&,但若派生类重载了operator T(),可能意外触发转换而非调用预期函数 - 模板别名(如
using base_t = Base<derived></derived>)在派生类中定义后,不能用于继承列表(class D : base_t是非法的),必须写全Base<d></d>
CRTP 看似简单,真正难的是控制好模板实例化边界和访问权限——一旦出错,错误信息往往横跨十几行模板展开,定位点常不在你写的那行。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










