模版奇特递归模式(CRTP)的核心思想其实很直接:让一个派生类(Derived)将自己作为模板参数传递给它的基类(Base)。
|
|
这种看似自己继承自己的写法,主要用来解决 C++ 中一类特定问题:在编译期绑定行为,避免运行时的虚函数开销,同时实现代码的复用和扩展 -1-3-10。
它的主要应用场景可以分为以下三大类:
场景一:实现编译期多态(静态多态)
这是 CRTP 最经典的应用。它允许基类定义一套接口,而派生类提供具体实现,这一切都在编译期完成。
-
解决的问题:替代虚函数,消除虚函数表(vtable)带来的运行时开销(如指针间接寻址、难以内联优化)。
-
典型示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20template <typename T> struct Shape { void draw() const { // 编译时绑定到派生类的 drawImpl static_cast<const T*>(this)->drawImpl(); } }; struct Circle : Shape<Circle> { void drawImpl() const { std::cout << "Drawing a Circle\n"; } }; struct Square : Shape<Square> { void drawImpl() const { std::cout << "Drawing a Square\n"; } }; template<typename T> void render(const Shape<T>& shape) { shape.draw(); // 静态多态调用 } -
权衡:这是编译期绑定的,不支持运行时多态(例如,不能将
Circle和Square放入同一个vector<Shape*>中并正确调用)610。此外,模板代码可能会增加编译时间和最终二进制文件的大小。
🧩 场景二:实现代码复用与功能注入
CRTP 可以作为“混入”(Mixin)工具,将一些通用的功能注入到派生类中,避免为每个类重复编写相同逻辑。
-
对象计数器:统计每个类的实例数量,并且每个类的计数是独立的 310。
1 2 3 4 5 6 7 8 9 10 11 12 13template <typename T> struct ObjectCounter { static int objects_created; static int objects_alive; ObjectCounter() { ++objects_created; ++objects_alive; } ~ObjectCounter() { --objects_alive; } }; template <typename T> int ObjectCounter<T>::objects_created = 0; template <typename T> int ObjectCounter<T>::objects_alive = 0; class MyClass : public ObjectCounter<MyClass> { }; class YourClass : public ObjectCounter<YourClass> { }; // MyClass 和 YourClass 拥有独立的计数器 -
标准库中的例子
std::enable_shared_from_this:这个类允许你在成员函数中安全地创建一个指向当前对象的shared_ptr。它正是通过 CRTP 实现的:你的类MyClass继承自enable_shared_from_this<MyClass>,从而获得了shared_from_this()方法 34。 -
多态复制(克隆):在基类中提供统一的
clone()接口,派生类无需重复编写克隆代码 3。
🏗️ 场景三:实现策略模式与扩展接口
CRTP 可以用来构建灵活的框架,允许用户通过派生类来提供特定的策略或扩展基类的功能。
- 策略模式:基类定义算法的骨架,派生类通过 CRTP 提供特定步骤的实现。这类似于模板方法模式,但避免了虚函数 48。
- 运算符重载:在编写数值计算库(如 Eigen、Boost.Units)时,可以在 CRTP 基类中统一实现
+、、 等运算符,这些运算符操作的是派生类类型,从而为所有派生类自动提供一致的运算接口 310。 - ATL/WTL 库:微软的 ATL 和 WTL 库广泛使用 CRTP 来允许用户类继承窗口实现类,从而获得处理窗口消息等能力,这是一种典型的接口扩展 13。
💎 总结
CRTP 是一种“零成本抽象”的典范,它把工作从运行时转移到了编译期。选择使用 CRTP 还是传统的虚函数,主要看你的需求:
- 如果你需要运行时多态(例如,处理一个容器中不同类型的对象),虚函数是合适的选择。
- 如果你追求极致性能、需要在编译期绑定行为,或者想为类注入通用功能,那么 CRTP 是一个非常强大的工具。