
上篇聊了类型萃取和SFINAE今天讲模板编程里一个很巧妙的设计模式——CRTPCuriously Recurring Template Pattern。名字听着挺唬人奇怪的递归模板模式。但原理其实不复杂。面试的时候面试官问除了虚函数你还能用什么方式实现多态我当时完全不知道有CRTP这个东西瞎扯了一通。后来学了才发现这个模式在实际项目里用得还挺多。CRTP的基本结构CRTP的核心就一句话派生类把自己作为模板参数传给基类。templatetypename Derived class Base { public: void interface() { // 调用派生类的方法 static_castDerived*(this)-implementation(); } void commonLogic() { cout Base common logic endl; } }; class ConcreteA : public BaseConcreteA { public: void implementation() { cout ConcreteA implementation endl; } }; class ConcreteB : public BaseConcreteB { public: void implementation() { cout ConcreteB implementation endl; } }; // 使用 ConcreteA a; a.interface(); // 调用ConcreteA的implementation ConcreteB b; b.interface(); // 调用ConcreteB的implementation奇怪在哪ConcreteA继承自BaseConcreteA——基类的模板参数是派生类自己。看起来像是循环引用但这是合法的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版本编译期多态 templatetypename Derived class SensorBase { public: void read() { static_castDerived*(this)-readImpl(); } }; class Lidar : public SensorBaseLidar { public: void readImpl() { /* 读激光雷达 */ } }; // 零运行时开销 Lidar lidar; lidar.read(); // 编译期直接绑定到Lidar::readImpl虚函数版本的调用路径read() → 查vtable → 找到函数地址 → 调用。CRTP版本的调用路径read() → 直接调用readImpl()。少了一次间接跳转。在机器人开发里控制回路里的函数调用对延迟很敏感。如果某个接口被高频调用比如1000Hz的IMU读取虚函数的开销虽然小几个纳秒但在极端场景下还是要优化的。CRTP的实际应用场景CRTP最经典的用法是Mixin模式——给类混入一些通用功能。比如你想给多个类都加上计数实例的功能templatetypename Derived class InstanceCounter { static int count_; public: InstanceCounter() { count_; } ~InstanceCounter() { count_--; } static int getInstanceCount() { return count_; } }; templatetypename T int InstanceCounterT::count_ 0; class LidarDriver : public InstanceCounterLidarDriver { // 自动获得了实例计数功能 }; class IMUDriver : public InstanceCounterIMUDriver { // 也有自己的实例计数 }; LidarDriver l1, l2; cout LidarDriver::getInstanceCount() endl; // 2 IMUDriver i1; cout IMUDriver::getInstanceCount() endl; // 1每个派生类都有自己独立的count_因为模板参数不同生成的是不同的基类实例。在机器人开发里CRTP还可以用于实现类型安全的基类接口。比如一个事件系统基类定义事件处理的框架派生类只需要实现具体的处理逻辑templatetypename Derived class EventHandler { public: void handle(const Event e) { if (canHandle(e)) { static_castDerived*(this)-onEvent(e); } } private: bool canHandle(const Event e) { return static_castDerived*(this)-filter(e); } };面试中的考点CRTP和虚函数相比有什么优缺点CRTP没有运行时开销编译期绑定但不能在运行时动态切换类型——你必须在编译期就知道具体类型。虚函数有运行时开销但支持运行时多态——可以通过基类指针调用派生类的方法。CRTP有什么限制最大的限制是不能用基类指针指向不同类型的派生类。BaseA和BaseB是不同的类型不能放在同一个容器里。如果你需要异构集合还是得用虚函数。static_cast在这里安全吗安全。因为CRTP的结构保证了Derived确实是Base的派生类static_cast在这里等价于一个已知的向下转型。再补充一个CRTP在机器人开发中的实际应用场景混入Mixin模式。你可以用CRTP给类动态地混入新功能而不需要继承链越来越深。比如你有一个传感器基类想给某些传感器加上日志功能、加上线程安全功能、加上性能统计功能用CRTP可以这样写class LidarSensor : public LoggableThreadingSafeSensorBase。每一层混入只添加自己负责的功能代码复用度极高而且因为是编译期绑定没有虚函数的运行时开销。这种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篇 设计模式之单例与工厂——机器人传感器管理中的实际应用有任何问题欢迎评论区留言我会尽量回复。