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 的核心关系:
10.6 什么时候用 CRTP,什么时候用虚函数?
我个人的经验是:
- 性能敏感的场景(每秒调用百万次以上)→ CRTP
- 需要运行时多态的场景(比如插件系统、动态加载)→ 虚函数
- 功能组合的场景(给类加各种能力)→ Mixin(CRTP 实现)
- 接口统一但实现各异的场景 → 两者皆可,看性能要求
说白了,CRTP 和虚函数不是互斥的。我见过一个项目里,底层用 CRTP 做高性能计算,上层用虚函数做策略模式。各取所长,才是正道。
总结:CRTP 是 C++ 模板元编程的基石之一。它让你在编译期就完成「继承」和「多态」,没有运行时开销。Mixin 则是 CRTP 的优雅应用,让你像搭积木一样组合功能。这两个模式,值得你花时间吃透。
公众号:蓝海资料掘金营,微信 deep3321