需要预先学习:


1. CRTP(奇异递归模板模式)

CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)是一种继承写法:派生类把自己作为模版参数传给基类。

template <typename Derived>
class Base
{
    // 基类通过模版参数 Derived "知道"派生类是谁
};

class Widget : public Base<Widget>   // Widget 把自己传给 Base
{
};

这样基类在编译期就知道"谁继承了我",可以直接使用派生类的类型——由此得到一类不依赖 virtual 的多态能力,称为静态多态(static polymorphism,编译期多态)。


1.1 最小示例

#include <iostream>

template <typename Derived>
class Base
{
public:
    void interface()
    {
        // 把 this 转换成派生类指针,调用派生类的实现
        static_cast<Derived*>(this)->impl();
    }
};

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

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

int main()
{
    Widget w;
    Gadget g;
    w.interface();   // 编译期绑定到 Widget::impl
    g.interface();   // 编译期绑定到 Gadget::impl
}

编译并运行代码后,看到这样的输出:

Widget::impl
Gadget::impl

interface() 是基类里的代码,却能调用派生类的 impl()——效果类似虚函数,但调用在编译期就已确定:没有查表开销,还能被编译器内联(inline,把函数体直接嵌入调用处,省去调用本身的开销)。


1.2 为什么叫"奇异递归"

拆开这个名字看:

  • 递归:Widget 的定义引用了 Base<Widget>,而 Base<Widget> 的成员又引用了 Widget——你中有我,我中有你,像是在"自己使用自己"
  • 奇异:Widget 还没定义完(正处在基类列表的位置),怎么就能当模版实参用?

答案在于模版实例化的时机。实例化 Base<Widget> 时,编译器只需要 Widget 这个名字,不需要它的完整定义,所以 Widget 此时是不完整类型也没关系;而成员函数 interface() 的函数体要等到被调用时才实例化——那时 Widget 已经定义完毕,static_cast<Derived*>(this) 完全合法。

注意:这条时机规则也划出了红线——基类的构造函数和析构函数里不能做这个转换。执行基类构造、析构时,派生类部分尚未构造或已经销毁,static_cast<Derived*>(this) 是未定义行为。


1.3 静态多态

先把同一件事用虚函数做一遍——统一的 draw 接口、多种图形:

#include <iostream>
#include <memory>
#include <vector>

class Shape
{
public:
    virtual void draw() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape
{
public:
    void draw() const override { std::cout << "画圆\n"; }
};

class Square : public Shape
{
public:
    void draw() const override { std::cout << "画方\n"; }
};

int main()
{
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>());
    shapes.push_back(std::make_unique<Square>());

    for(const auto& s : shapes)
        s->draw();   // 运行期查虚表,决定调用哪个 draw
}

虚函数的代价:每个对象多携带一个 vptr(virtual table pointer,指向虚函数表的指针),每次调用都要经虚表间接跳转,编译器难以内联。当具体类型在编译期已经确定时,这些代价可以完全省掉——CRTP 版本:

#include <iostream>

template <typename Derived>
class ShapeBase
{
public:
    void draw() const
    {
        static_cast<const Derived*>(this)->draw_impl();
    }
};

class Circle : public ShapeBase<Circle>
{
public:
    void draw_impl() const { std::cout << "画圆\n"; }
};

class Square : public ShapeBase<Square>
{
public:
    void draw_impl() const { std::cout << "画方\n"; }
};

// 接口由基类统一,实现由派生类提供,泛型代码直接可用
template <typename T>
void render(const T& shape)
{
    shape.draw();
}

int main()
{
    Circle c;
    Square s;
    render(c);   // 编译期确定调用 Circle::draw_impl
    render(s);   // 编译期确定调用 Square::draw_impl
}

两种多态的对比:

虚函数多态 CRTP 静态多态
绑定时机 运行期(查虚表) 编译期
调用开销 间接跳转,难内联 零开销,可内联
对象大小 多一个 vptr 无额外开销
公共基类类型 有,Shape* 可指向任何派生 没有,ShapeBase<A> 与 ShapeBase<B> 是不同类型
异构容器 vector<Shape*> 直接可用 困难,缺少统一类型
运行期决定类型 支持(配置、插件) 不支持

一句话:虚函数用一点运行期开销换取运行期灵活性;CRTP 放弃这种灵活性,换来零开销和内联机会。


1.4 应用:把通用功能注入基类

每个类独立的实例计数器

统计"每个类当前有多少个存活实例"。用普通继承会出问题:静态成员只有一份,会被所有派生类共享。CRTP 天然解决——Counter<Widget> 与 Counter<Gadget> 是不同的类,各自持有独立的静态变量:

#include <iostream>

template <typename T>
class Counter
{
public:
    Counter()                  { ++m_count; }
    Counter(const Counter&)    { ++m_count; }
    ~Counter()                 { --m_count; }

    static size_t alive() { return m_count; }

private:
    inline static size_t m_count = 0;
};

class Widget : public Counter<Widget> {};
class Gadget : public Counter<Gadget> {};

int main()
{
    Widget w1, w2;
    Gadget g1;

    std::cout << Widget::alive() << '\n';   // 2
    std::cout << Gadget::alive() << '\n';   // 1
}

派生类一行代码没写,就获得了"只属于自己"的计数器。这是 CRTP 最典型的用法:把通用功能注入派生类,同时为每个派生类生成一份独立的实现。

只写一个比较运算符,基类补全全套

派生类只实现 operator<,其余比较运算符由基类借它推导出来:

#include <iostream>

template <typename Derived>
class Comparable
{
public:
    bool operator>(const Derived& rhs) const
    {
        return rhs < static_cast<const Derived&>(*this);
    }

    bool operator==(const Derived& rhs) const
    {
        const Derived& self = static_cast<const Derived&>(*this);
        return !(self < rhs) && !(rhs < self);
    }
};

class Money : public Comparable<Money>
{
public:
    explicit Money(int cents) : m_cents(cents) {}

    bool operator<(const Money& rhs) const { return m_cents < rhs.m_cents; }

private:
    int m_cents;
};

int main()
{
    Money a(100), b(200);

    std::cout << std::boolalpha;
    std::cout << (a > b) << '\n';   // false,由基类的 operator> 完成
    std::cout << (a == b) << '\n';  // false,由基类的 operator== 完成
}

注:C++20 的三向比较 <=> 可以自动生成全套比较运算符,这个经典用法弱化了,但它体现的"基类借派生类之力"的思想依然是 CRTP 的核心。

链式调用不被基类截断

基类方法返回 static_cast<Derived&>(*this) 而不是基类引用,链式调用就不会被"困"在基类的方法集里:

#include <iostream>
#include <string>

template <typename Derived>
class Printer
{
public:
    Derived& indent()
    {
        std::cout << "    ";
        return static_cast<Derived&>(*this);   // 关键:返回派生类引用
    }
};

class Report : public Printer<Report>
{
public:
    Report& line(const std::string& text)
    {
        std::cout << text << '\n';
        return *this;
    }
};

int main()
{
    Report r;
    r.indent().line("标题").indent().line("内容");
}

如果 indent() 返回的是 Printer&,第二条链只能继续调用基类方法——line() 不在基类里,链就断了。


1.5 应用:std::enable_shared_from_this

标准库中最著名的 CRTP 是 std::enable_shared_from_this。它解决的问题是:对象在自己的成员函数里,安全地拿到管理自己的 shared_ptr。

直接拿 this 构造是错误的做法:

std::shared_ptr<Task> self(this);   // 错误!为同一个对象新建了独立的引用计数块

外界的 shared_ptr 和这里的 self 各计各的数,对象会被 delete 两次。正确做法是继承 enable_shared_from_this<自己>:

#include <iostream>
#include <memory>

class Task : public std::enable_shared_from_this<Task>
{
public:
    void run()
    {
        // 从基类提升出共享同一控制块的 shared_ptr
        std::shared_ptr<Task> self = shared_from_this();
        std::cout << "当前引用计数: " << self.use_count() << '\n';
    }
};

int main()
{
    auto task = std::make_shared<Task>();
    task->run();   // 输出 2(task 和 self 各持有一个引用)
}

原理:enable_shared_from_this 基类内部藏着一个 weak_ptr,对象首次被 shared_ptr 接管时会自动登记进去,shared_from_this() 从这个 weak_ptr 提升出 shared_ptr——控制块自始至终只有一份。典型场景是异步回调:把 self 存进回调,延长对象存活到回调执行完毕。

注意:shared_from_this() 只能在对象已经被 shared_ptr 接管之后调用——栈对象上调用、或在构造函数里调用,都会抛出 std::bad_weak_ptr。


1.6 局限与选择

CRTP 的短板都来自"编译期绑定"这枚硬币的另一面:

  • ShapeBase<Circle> 与 ShapeBase<Square> 是不同类型,没有公共基类可用——两种图形的指针装不进同一个容器,运行期也无法在它们之间切换
  • 模版代码都在头文件里,每种派生类都实例化一份,编译时间和二进制体积随之膨胀
  • 调用栈比直接写多一层基类转发,偶尔影响调试体验

选择建议:

  • 虚函数:需要运行期灵活性——异构容器、按配置选实现、跨模块接口
  • CRTP:类型编译期已知、在意开销(数值计算、游戏、嵌入式),或想给类注入通用功能

两者并不互斥,enable_shared_from_this 就常常用在带虚函数的类上。另外,Eigen 这类线性代数库用 CRTP 实现表达式模版(expression templates),把 a + b * c 编译成不带临时对象的循环代码——那是 CRTP 性向的极致发挥。