10. 模板与继承:CRTP(奇异递归模板模式)、混入(Mixin)风格编程

模板和继承,这两个东西放在一起,很多人第一反应是「虚函数、多态、运行时」。但今天我要聊的,是编译期就能把继承玩出花来的技术——CRTP 和 Mixin。说白了,就是让基类知道派生类是谁,然后在编译期就把活儿干了。

我个人习惯把 CRTP 叫做「模板版的自我指涉」。你想想看,一个类把自己的派生类作为模板参数传给基类,这听起来有点绕,但用起来是真香。

10.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";
    }
};

看到没?Base<Derived> 这个写法,就是 CRTP 的核心。基类通过 static_cast 把自己转成派生类,然后调用派生类的方法。这跟虚函数不同——没有 vtable,没有运行时开销,纯编译期多态。

关键区别:虚函数是运行时多态,CRTP 是编译期多态。前者灵活但慢,后者快但死板。选哪个?看场景。

10.2 我在项目中遇到的 CRTP 实战

我记得有一次做高性能计算库,需要给不同的数值类型提供统一的接口。如果用虚函数,每次调用都要查 vtable,性能损失受不了。我当时就用 CRTP 实现了「静态接口」:

template <typename T>
class VectorBase {
public:
    T& derived() { return static_cast<T&>(*this); }
    const T& derived() const { return static_cast<const T&>(*this); }

    double norm() const {
        return derived().do_norm();
    }

    void scale(double factor) {
        derived().do_scale(factor);
    }
};

class Vec3 : public VectorBase<Vec3> {
    double x, y, z;
public:
    double do_norm() const {
        return std::sqrt(x*x + y*y + z*z);
    }
    void do_scale(double f) {
        x *= f; y *= f; z *= f;
    }
};

这样做的好处是:norm()scale() 是基类提供的公共接口,但具体实现由派生类决定。而且——没有虚函数开销。我在做物理引擎的时候,每帧要调用几百万次 norm(),用 CRTP 比虚函数快了将近 30%。

10.3 混入(Mixin)风格编程

混入,说白了就是「按需组合功能」。你有一个基础类,想给它加一些能力,但又不想用多重继承搞得一团糟。这时候 Mixin 就派上用场了。

Mixin 通常也是用 CRTP 实现的。每个 Mixin 是一个模板类,它知道最终派生类是谁,然后注入自己的行为:

template <typename Base>
class LoggingMixin : public Base {
public:
    void do_something() {
        std::cout << "Before do_something\n";
        Base::do_something();
        std::cout << "After do_something\n";
    }
};

template <typename Base>
class TimingMixin : public Base {
public:
    void do_something() {
        auto start = std::chrono::high_resolution_clock::now();
        Base::do_something();
        auto end = std::chrono::high_resolution_clock::now();
        std::cout << "Took " << (end - start).count() << "ns\n";
    }
};

class Core {
public:
    void do_something() {
        // 核心逻辑
    }
};

// 组合使用
using EnhancedCore = LoggingMixin<TimingMixin<Core>>;

你看,EnhancedCore 既有日志功能,又有计时功能。而且这些功能是编译期组合的,没有运行时开销。我曾经在写一个网络库的时候,用这种手法给不同的协议处理器动态组合「压缩」「加密」「日志」等功能,代码干净得不得了。

小技巧:Mixin 的模板参数链可以任意长。但要注意,每个 Mixin 都要调用 Base::xxx(),否则链会断掉。我习惯在文档里明确标注「必须调用基类版本」。

10.4 避坑指南

我曾经踩过一个坑:CRTP 的基类构造函数里调用了派生类的方法。结果呢?派生类还没构造完,调用的方法访问了未初始化的成员变量,程序直接崩了。嗯,这里要注意——不要在 CRTP 基类的构造函数里调用派生类的方法。派生类在基类构造时还没完全构造好呢。

另一个坑是:CRTP 的派生类如果被多重继承,static_cast 可能会出问题。我建议每个 CRTP 基类只被一个派生类继承,保持一对一的关系。

警告:CRTP 和虚函数不能混用在同一接口上。如果你既想用 CRTP 的静态多态,又想用虚函数的动态多态,那设计上一定有问题。我见过有人把 CRTP 基类里的方法又声明成 virtual,结果搞得四不像——既没有静态多态的性能,也没有动态多态的灵活性。

10.5 知识体系图

下面这张图总结了 CRTP 和 Mixin 的核心关系:

CRTP 与 Mixin 知识体系 CRTP 核心模式 • 基类模板参数 = 派生类 • static_cast 实现静态多态 • 编译期绑定,无 vtable 开销 • 典型应用:静态接口、代码复用 Mixin 风格编程 • 基于 CRTP 的功能组合 • 模板链式继承 • 按需注入行为(日志、计时等) • 编译期组合,零运行时开销 典型应用场景 1. 数值计算库:统一接口 + 高性能(无虚函数) 2. 网络协议栈:组合压缩、加密、日志等中间件 3. 游戏引擎:组件系统、行为树节点 4. 序列化框架:自动生成序列化/反序列化代码 ⚠ 注意:不要在 CRTP 基类构造函数中调用派生类方法;避免多重继承冲突 图:CRTP 与 Mixin 的关系及典型应用

10.6 什么时候用 CRTP,什么时候用虚函数?

我个人的经验是:

  • 性能敏感的场景(每秒调用百万次以上)→ CRTP
  • 需要运行时多态的场景(比如插件系统、动态加载)→ 虚函数
  • 功能组合的场景(给类加各种能力)→ Mixin(CRTP 实现)
  • 接口统一但实现各异的场景 → 两者皆可,看性能要求

说白了,CRTP 和虚函数不是互斥的。我见过一个项目里,底层用 CRTP 做高性能计算,上层用虚函数做策略模式。各取所长,才是正道。

总结:CRTP 是 C++ 模板元编程的基石之一。它让你在编译期就完成「继承」和「多态」,没有运行时开销。Mixin 则是 CRTP 的优雅应用,让你像搭积木一样组合功能。这两个模式,值得你花时间吃透。


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