19、奇异递归模板模式(CRTP):静态多态、代码复用、计数器模式

CRTP,全称是 Curiously Recurring Template Pattern。名字挺长,说白了就是:派生类把自己作为模板参数传给基类

我第一次看到这种写法时,心里想的是:「这玩意儿不会死循环吗?」
后来用多了才发现——它不但不死循环,反而是 C++ 模板元编程里最实用的技巧之一。

今天我们就来聊聊 CRTP 的三个核心应用场景:静态多态代码复用计数器模式


19.1 什么是 CRTP?

先看一个最简单的例子:

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class Derived : public Base<Derived> {
public:
    void implementation() {
        std::cout << "Derived implementation\n";
    }
};

注意看:Derived 继承自 Base<Derived>。这就是 CRTP 的核心——派生类把自己作为模板参数传给基类

基类里通过 static_cast<Derived*>(this) 来调用派生类的成员函数。这个转换在编译期就完成了,没有虚函数开销。

关键点:CRTP 实现的是编译期多态,不是运行期多态。没有 vtable,没有虚函数调用开销。

19.2 静态多态:没有虚函数的多态

传统多态靠虚函数,运行时查虚表。CRTP 的多态在编译期就确定了。

我举个例子。假设我们要写一个「比较器」框架:

template <typename Derived>
class Comparator {
public:
    bool operator!=(const Derived& other) const {
        return !static_cast<const Derived*>(this)->operator==(other);
    }

    bool operator>(const Derived& other) const {
        return other < *static_cast<const Derived*>(this);
    }

    // 其他比较运算符...
};

class IntWrapper : public Comparator<IntWrapper> {
    int value_;
public:
    explicit IntWrapper(int v) : value_(v) {}

    bool operator==(const IntWrapper& other) const {
        return value_ == other.value_;
    }

    bool operator<(const IntWrapper& other) const {
        return value_ < other.value_;
    }
};

你只需要实现 ==<,基类自动帮你生成 !=>>=<=

我在项目中遇到过类似的需求——需要为几十种数值类型提供完整的比较运算符。用 CRTP 写一次基类,所有派生类自动补齐,省了至少一半的代码量。

避坑指南:我曾经在 CRTP 基类里不小心写了虚函数,结果编译器生成了 vtable,静态多态变成了动态多态。记住:CRTP 里不要用 virtual,否则就失去了它的意义。

19.3 代码复用:把公共逻辑抽到基类

CRTP 的第二个大用处是代码复用。你想想看,很多类都有相同的「骨架」,只是「血肉」不同。CRTP 可以把骨架抽出来。

比如,我们要实现一个「单例模式」:

template <typename T>
class Singleton {
public:
    static T& instance() {
        static T obj;
        return obj;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

protected:
    Singleton() = default;
};

class Logger : public Singleton<Logger> {
    friend class Singleton<Logger>;
    // Logger 自己的实现...
public:
    void log(const std::string& msg) { /* ... */ }
};

所有单例类只需要继承 Singleton<Derived>,自动获得 instance() 方法。注意 friend class Singleton<Logger> 这一行——因为基类的构造函数是 protected,派生类需要让基类能访问它。

再比如,对象池、工厂模式、访问者模式……很多设计模式都可以用 CRTP 来复用公共逻辑。

我的习惯:只要发现两个类有相同的「流程骨架」,我就考虑用 CRTP 把骨架抽到基类里。派生类只提供「差异点」。

19.4 计数器模式:编译期自增 ID

这个用法比较冷门,但非常巧妙。CRTP 可以配合静态成员实现「编译期计数器」。

看代码:

template <typename T>
class Counter {
public:
    static int count() { return counter_; }

protected:
    Counter() { ++counter_; }
    ~Counter() { --counter_; }

private:
    static inline int counter_ = 0;
};

class Widget : public Counter<Widget> { /* ... */ };
class Gadget : public Counter<Gadget> { /* ... */ };

每个派生类都有自己的 counter_ 静态变量。因为 Counter<Widget>Counter<Gadget>不同的模板实例化,它们的静态成员互不干扰。

你可以用这个来:

  • 统计某个类的当前存活对象数量
  • 给每个对象分配一个自增 ID
  • 实现对象池的引用计数

我曾经用这个模式写过一个调试工具——在析构函数里打印当前对象数量,排查内存泄漏。效果拔群。

注意:这个计数器不是「编译期常量」,而是「运行期计数」。它利用的是模板实例化的独立性,不是 constexpr 计算。如果你需要真正的编译期自增 ID(比如给类型编号),需要用 __COUNTER__ 宏或者更复杂的模板技巧。

19.5 知识体系总览

下面这张图总结了 CRTP 的三大应用场景和它们之间的关系:

CRTP 核心 静态多态 无虚函数开销 编译期绑定 适合性能敏感场景 代码复用 单例模式 比较运算符自动生成 对象池/工厂模式 计数器模式 对象存活计数 自增 ID 分配 调试/内存泄漏检测 本质:编译期多态 + 模板实例化独立性 基类通过 static_cast<Derived*>(this) 调用派生类方法

19.6 总结与避坑

CRTP 是 C++ 模板元编程里「性价比」最高的技巧之一。它不复杂,但能解决很多实际问题。

最后列几个我踩过的坑:

  • 不要滥用 dynamic_cast:CRTP 基类里用 static_cast 就够了,别用 dynamic_cast,那会引入运行期开销。
  • 注意友元声明:如果基类的构造函数是 protected,派生类需要 friend class Base<Derived> 才能让基类访问。
  • 模板代码膨胀:每个不同的模板参数都会生成一份独立的代码。如果派生类很多,注意二进制体积。
  • 不要和虚函数混用:CRTP 和虚函数是两种不同的多态机制。混用会让代码难以理解,而且可能失去性能优势。

嗯,CRTP 就聊到这里。它不是什么高深莫测的技术,但用好了,能让你的代码更干净、更高效。


一句话总结:CRTP 是「派生类把自己传给基类」的模板技巧,用来实现编译期多态、复用代码、以及对象计数。没有虚函数开销,适合性能敏感场景。

公众号:蓝海资料掘金营,微信deep3321