CRTP模式:面试官问“不用虚函数怎么实现多态“,我完全没听过这个词
上篇聊了类型萃取和SFINAE,今天讲模板编程里一个很巧妙的设计模式——CRTP(Curiously Recurring Template Pattern)。
名字听着挺唬人,"奇怪的递归模板模式"。但原理其实不复杂。
面试的时候,面试官问"除了虚函数,你还能用什么方式实现多态?"我当时完全不知道有CRTP这个东西,瞎扯了一通。后来学了才发现,这个模式在实际项目里用得还挺多。
CRTP的基本结构
CRTP的核心就一句话:派生类把自己作为模板参数传给基类。
template<typename Derived>
class Base {
public:
void interface() {
// 调用派生类的方法
static_cast<Derived*>(this)->implementation();
}
void commonLogic() {
cout << "Base common logic" << endl;
}
};
class ConcreteA : public Base<ConcreteA> {
public:
void implementation() {
cout << "ConcreteA implementation" << endl;
}
};
class ConcreteB : public Base<ConcreteB> {
public:
void implementation() {
cout << "ConcreteB implementation" << endl;
}
};
// 使用
ConcreteA a;
a.interface(); // 调用ConcreteA的implementation
ConcreteB b;
b.interface(); // 调用ConcreteB的implementation
"奇怪"在哪?ConcreteA继承自Base<ConcreteA>——基类的模板参数是派生类自己。看起来像是循环引用,但这是合法的C++。
为什么用CRTP而不用虚函数
虚函数实现多态,大家都知道。但虚函数有运行时代价——每次调用都要查虚函数表,有一次间接跳转。
CRTP实现的是静态多态——在编译期就确定了调用哪个函数,没有运行时开销。
// 虚函数版本:运行时多态
class SensorBase {
public:
virtual void read() = 0;
virtual ~SensorBase() = default;
};
class Lidar : public SensorBase {
public:
void read() override { /* 读激光雷达 */ }
};
// 有虚函数表开销
SensorBase* sensor = new Lidar();
sensor->read(); // 查虚函数表,间接调用
// CRTP版本:编译期多态
template<typename Derived>
class SensorBase {
public:
void read() {
static_cast<Derived*>(this)->readImpl();
}
};
class Lidar : public SensorBase<Lidar> {
public:
void readImpl() { /* 读激光雷达 */ }
};
// 零运行时开销
Lidar lidar;
lidar.read(); // 编译期直接绑定到Lidar::readImpl
虚函数版本的调用路径:read() → 查vtable → 找到函数地址 → 调用。CRTP版本的调用路径:read() → 直接调用readImpl()。少了一次间接跳转。
在机器人开发里,控制回路里的函数调用对延迟很敏感。如果某个接口被高频调用(比如1000Hz的IMU读取),虚函数的开销虽然小(几个纳秒),但在极端场景下还是要优化的。
CRTP的实际应用场景
CRTP最经典的用法是Mixin模式——给类"混入"一些通用功能。
比如你想给多个类都加上"计数实例"的功能:
template<typename Derived>
class InstanceCounter {
static int count_;
public:
InstanceCounter() { count_++; }
~InstanceCounter() { count_--; }
static int getInstanceCount() { return count_; }
};
template<typename T>
int InstanceCounter<T>::count_ = 0;
class LidarDriver : public InstanceCounter<LidarDriver> {
// 自动获得了实例计数功能
};
class IMUDriver : public InstanceCounter<IMUDriver> {
// 也有自己的实例计数
};
LidarDriver l1, l2;
cout << LidarDriver::getInstanceCount() << endl; // 2
IMUDriver i1;
cout << IMUDriver::getInstanceCount() << endl; // 1
每个派生类都有自己独立的count_,因为模板参数不同,生成的是不同的基类实例。
在机器人开发里,CRTP还可以用于实现类型安全的基类接口。比如一个事件系统,基类定义事件处理的框架,派生类只需要实现具体的处理逻辑:
template<typename Derived>
class EventHandler {
public:
void handle(const Event& e) {
if (canHandle(e)) {
static_cast<Derived*>(this)->onEvent(e);
}
}
private:
bool canHandle(const Event& e) {
return static_cast<Derived*>(this)->filter(e);
}
};
面试中的考点
"CRTP和虚函数相比有什么优缺点?"CRTP没有运行时开销(编译期绑定),但不能在运行时动态切换类型——你必须在编译期就知道具体类型。虚函数有运行时开销,但支持运行时多态——可以通过基类指针调用派生类的方法。
"CRTP有什么限制?"最大的限制是不能用基类指针指向不同类型的派生类。Base<A>和Base<B>是不同的类型,不能放在同一个容器里。如果你需要异构集合,还是得用虚函数。
"static_cast在这里安全吗?"安全。因为CRTP的结构保证了Derived确实是Base的派生类,static_cast在这里等价于一个已知的向下转型。
再补充一个CRTP在机器人开发中的实际应用场景:混入(Mixin)模式。你可以用CRTP给类动态地"混入"新功能,而不需要继承链越来越深。比如你有一个传感器基类,想给某些传感器加上日志功能、加上线程安全功能、加上性能统计功能,用CRTP可以这样写:class LidarSensor : public Loggable<ThreadingSafe<SensorBase>>。每一层混入只添加自己负责的功能,代码复用度极高,而且因为是编译期绑定,没有虚函数的运行时开销。这种Mixin模式在Boost和Eigen库里用得非常多。面试时如果你能聊到CRTP和Mixin的结合应用,面试官会觉得你对C++模板编程的理解已经相当深入了。
给正在准备面试的你
CRTP在面试里属于"知道就知道,不知道就完全没思路"的知识点。它不是每天都会用到的技术,但它展示了C++模板系统的一种巧妙用法。
面试的时候,能讲清楚CRTP的结构、和虚函数的区别、以及适用场景,说明你对C++的多态机制有全面的理解。
其实CRTP在Boost库里用得非常多。比如Boost.EnableSharedFromThis就是CRTP的经典应用,让对象能安全地获取指向自身的shared_ptr。还有Boost.Iterator库也用CRTP来避免虚函数的开销。面试时如果你能提到"Boost库里大量使用CRTP来实现零开销抽象",说明你不只是知道语法,还了解工业级的代码实践。
下篇进入设计模式专题。先聊单例模式和工厂模式——机器人传感器管理中的实际应用。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列 上一篇:第44篇 类型萃取与SFINAE——模板元编程的入门理解 下一篇预告:第46篇 设计模式之单例与工厂——机器人传感器管理中的实际应用
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)