CRTP 惯用法
奇异递归模板模式
CRTP 惯用法 是 CoddyKit 上的免费 C++ Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C++ Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C++ Academy 课程共包含 4 节课。
什么是 CRTP
奇特递归模板模式(CRTP)是一种 C++ 惯用法:类 Derived 继承自使用 Derived 自身实例化的模板基类。
- 形式:
class Derived : public Base<Derived> - 基类在编译期获知其派生类型
template <typename T>
class Base {};
class Derived : public Base<Derived> {};核心机制
在基类内部,您可以将 this 指针 static_cast 为派生类型。由于派生类型是模板参数,这是完全发生在编译期的转换,不会产生运行时开销。
#include <iostream>
template <typename T>
class Base {
public:
void interface() {
static_cast<T*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() { std::cout << "Derived impl\n"; }
};
int main() {
Derived d;
d.interface();
return 0;
}为什么叫“奇特递归”
这个名称源于一个表面上的悖论:基类会引用派生类,而编写基类模板时,派生类尚未完全定义。
- 它之所以可行,是因为模板只有在使用时才会实例化
- 实例化时,
Derived已经是完整类型
第一个实际用途
CRTP 让基类能够提供可复用的功能,并回调派生类。这里,基类提供了一个 print(),将工作委托给派生类的 name()。
#include <iostream>
#include <string>
template <typename T>
struct Named {
void print() {
std::cout << static_cast<T*>(this)->name() << "\n";
}
};
struct Cat : Named<Cat> {
std::string name() { return "Cat"; }
};
int main() {
Cat c;
c.print();
return 0;
}没有虚函数表
与运行时多态不同,CRTP 不需要 virtual 关键字,也不需要在每个对象中保存虚函数表指针。分派完全在编译期完成。
- 对象更小(没有隐藏的虚函数表指针)
- 调用可以被内联
统计实例数量
CRTP 的一个经典应用是对象计数器。每个派生类都会自动获得自己的静态计数器。
#include <iostream>
template <typename T>
struct Counter {
static inline int count = 0;
Counter() { ++count; }
~Counter() { --count; }
};
struct Widget : Counter<Widget> {};
int main() {
Widget a, b;
std::cout << Widget::count << "\n";
return 0;
}每个 Derived 都有自己的状态
由于 Counter<Widget> 和 Counter<Gadget> 是不同的模板实例化,每个实例都有独立的静态 count。使用单个非模板基类无法实现这种隔离。
#include <iostream>
template <typename T>
struct Counter {
static inline int count = 0;
Counter() { ++count; }
};
struct A : Counter<A> {};
struct B : Counter<B> {};
int main() {
A a1, a2;
B b1;
std::cout << A::count << " " << B::count << "\n";
return 0;
}这里的 static_cast 是安全的
只有当 Base<T> 类型的每个对象确实都是 T 时,基类内部的向下转换才是安全的。CRTP 从结构上保证了这一点,因为 T 就是继承该基类的类。
- 绝不要为不相关的类实例化
Base<Wrong>
通过 CRTP 实现比较运算符
通过继承比较辅助类,您可以从单个 operator< 生成完整的一组比较运算符。
#include <iostream>
template <typename T>
struct Comparable {
friend bool operator>(const T& a, const T& b) { return b < a; }
friend bool operator==(const T& a, const T& b) { return !(a < b) && !(b < a); }
};
struct Num : Comparable<Num> {
int v;
Num(int x) : v(x) {}
friend bool operator<(const Num& a, const Num& b) { return a.v < b.v; }
};
int main() {
std::cout << (Num(3) > Num(2)) << "\n";
return 0;
}受保护的构造函数防护
为了防止基类被意外误用,可以将其构造函数设为 protected,这样只有派生类才能创建它。
#include <iostream>
template <typename T>
class Base {
protected:
Base() = default;
public:
void run() { static_cast<T*>(this)->step(); }
};
class Job : public Base<Job> {
public:
void step() { std::cout << "step\n"; }
};
int main() {
Job j;
j.run();
return 0;
}标准库中的 CRTP
CRTP 广泛存在于真实的 C++ 库中。
std::enable_shared_from_this<T>使用了它- 许多表达式模板数学库(Eigen)依赖它
- 范围和迭代器辅助工具也使用类似模式
识别出 X : Base<X> 这种形式,有助于您阅读高级代码。
快速检查
测试您对 CRTP 形式的理解。
回顾
您学习了 CRTP 惯用法。
Derived : Base<Derived>将具体类型传递给基类static_cast<T*>(this)可以实现编译期分派- 没有虚函数表,对象更小,调用可以内联
- 用于计数器、比较混入类和库辅助工具
用 AI 导师学习 C++ — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 51
- 课程
- 203
常见问题解答
「CRTP 惯用法」课时是免费的吗?
是的 — 「CRTP 惯用法」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C++ Academy 课程的其余内容,请升级到 CoddyKit PRO。 C++ Academy 课程共包含 4 节课。
「CRTP 惯用法」这节课中我会学到什么?
奇异递归模板模式 你通过在浏览器中直接运行的动手代码来练习 C++ Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C++ Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C++ Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「CRTP 惯用法」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C++ Academy 课中编写并运行代码吗?
能。每节 C++ Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- CRTP 惯用法
- 静态多态
- 使用 CRTP 实现混入
- 何时使用 CRTP