C++ CRTP模版奇特递归

C++ learning

模版奇特递归模式(CRTP)的核心思想其实很直接:让一个派生类(Derived)将自己作为模板参数传递给它的基类(Base)。

1
2
3
4
5
6
7
8
template <typename T>
class Base {
    // 基类中可以通过 static_cast<T*>(this) 来调用派生类的成员
};

class Derived : public Base<Derived> {
    // ...
};

这种看似自己继承自己的写法,主要用来解决 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
    20
    
    template <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(); // 静态多态调用
    }
    
  • 优势:相比虚函数,CRTP 没有运行时开销,调用可能被内联,性能更高 13

  • 权衡:这是编译期绑定的,不支持运行时多态(例如,不能将 Circle 和 Square 放入同一个 vector<Shape*> 中并正确调用)610。此外,模板代码可能会增加编译时间和最终二进制文件的大小。

🧩 场景二:实现代码复用与功能注入

CRTP 可以作为“混入”(Mixin)工具,将一些通用的功能注入到派生类中,避免为每个类重复编写相同逻辑。

  • 对象计数器:统计每个类的实例数量,并且每个类的计数是独立的 310

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    template <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 是一个非常强大的工具。
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy