上篇聊了类型萃取和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篇 设计模式之单例与工厂——机器人传感器管理中的实际应用

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐