C++之CRTP的使用

什么是CRTP

CRTP全称是curious recurring template pattern,即奇异递归模版模式,是一种c++的设计模式,精巧地结合了继承和模板编程的技术。可以用来给c++的class提供额外功能、实现静态多态等。

在CRTP之前只听说C++通过指针实现了动态多态,现在居然搞出了一个静态多态的东西出来?不得不感慨C++真是一门高深莫测的语言...

动态多态

在了解静态多态之前我们先来回顾一下动态多态,C++ 通过类的继承与虚函数的动态绑定,实现了多态。这种特性,使得我们能够用基类的指针,访问子类的实例。

c 复制代码
#include <iostream>

class Animal{
public:
    // 注意需要添加virtual关键字
    virtual void run(){
        std::cout << "Animal run" << std::endl;
    }
};

class Cat:public Animal{
public:
    void run() override{
        std::cout << "Cat run" << std::endl;
    }
};

int main() {
    std::vector<Animal> animalVec;
    animalVec.emplace_back(Animal());
    // 非指针的形式,其实内部调用的还是Animal的run
    animalVec.emplace_back(Cat());
    for (auto animal:animalVec) {
        animal.run();
    }
    // 动态多态需要通过指针的形式实现
    std::vector<Animal*> animalVecPtr;
    animalVecPtr.push_back(new Animal());
    animalVecPtr.push_back(new Cat());
    for (auto animal:animalVecPtr) {
        animal->run();
    }
    return 0;
}

CRTP的一个重要功能就是用来实现静态多态,CRTP在编译阶段就将子类类型以模版的形式传递到父类,以便在编译阶段实现多态性,这就是静态多态。

既然有了动态多态,为什么还需要静态多态呢?答案是精益求精,为了效率而生...

我们知道动态多态是基于虚函数的形式在运行时进行动态绑定的,因此每次运行时都需要查询虚函数表,所以动态绑定会降低程序的执行效率。 为了兼顾多态与效率,就提出了CRTP。

CRTP的使用

我们先来看看在cppreference中是如何使用CRTP的

下面我们依然使用上面Animal的例子通过CRTP的方式实现静态多态。

首先我们按照官方的例子,依瓢画葫芦:

arduino 复制代码
#include <iostream>

template  < class T >
class Animal{
public:
    virtual ~Animal(){

    };
    // CRTP这里已经不需要使用virtual关键字了
     void run(){
        (static_cast<T*>(this))->run();
    }
};

class Cat:public Animal<Cat>{
public:
    void run(){
        std::cout << "Cat run" << std::endl;
    }
};

class Dog:public Animal<Dog>{
public:
    void run(){
        std::cout << "Dog run" << std::endl;
    }
};

int main() {
    Animal<Cat>* cat = new Cat;
    cat->run();
    delete cat;
    Animal<Dog>* dog = new Dog;
    dog->run();
    delete dog;
    return 0;
}

程序运行起来后打印如下:

可以发现通过CRTP我们不使用关键字virtual也能实现了通过父类指针调用子类方法效果,这就是静态多态的优点,它比动态多态更高效,更安全。

通过上面的例子我们总结一下使用CRTP的三个重要步骤:

  • 继承自模版类,因为用到了继承,因此析构函数需要用virtual修饰,以避免内存泄露。

  • 子类将自身通过模板参数传递给父类。

  • 父类通过static_cast关键字将模板参数静态转化成子类,然后调用子类的鸭子模型方法。

一般来说将父类转换成子类一般使用的是dynamic_cast,而CRTP是在编译期间就已经明确知道了子类的具体类型,因此直接使用static_cast更为高效。 这也正是CRTP这种设计的一大精髓。

通过仔细对比我们动态多态和静态多态的两个例子我们发现还是有点不一样的,我们在动态多态中将Animal的指针添加到了std::vector中去,那么我们的CRTP能否也这样做呢? 我们来试一下:

arduino 复制代码
#include <iostream>

template<class T>
class Animal {
public:
    virtual ~Animal() {

    };
    // CRTP这里已经不需要使用virtual关键字了
    void run() {
        (static_cast<T *>(this))->run();
    }
};

class Cat : public Animal<Cat> {
public:
    void run() {
        std::cout << "Cat run" << std::endl;
    }
};

class Dog : public Animal<Dog> {
public:
    void run() {
        std::cout << "Dog run" << std::endl;
    }
};

int main() {
    std::vector<Animal<Cat>*> animalVec;
    animalVec.emplace_back(new Cat());
    // 报错了,因为vector存放的数据类型是Animal<Cat>
    animalVec.emplace_back(new Dog());
    for (auto animal: animalVec) {
        animal->run();
    }
    return 0;
}

我们发现报错了,因为Animal和Animal不是同样的数据类型,不能同时放入同一个vector中去。 既然问题的根源是他们不是同样的数据类型,那么我们将它们变成同样的数据类型不就是行了吗?那么怎么把它们变成同样的数据类型呢?

让它们继承一个共同的基类即可。这样就是动态多态与静态多态结合使用的例子了。

实例代码如下:

arduino 复制代码
#include <iostream>

class BaseAnimal {
public:
    virtual ~BaseAnimal() {

    };

    virtual void run() = 0;
};

template<class T>
class Animal: public BaseAnimal{
public:
    virtual ~Animal() {

    };
    // CRTP这里已经不需要使用virtual关键字了
    void run() override{
        (static_cast<T *>(this))->run();
    }
};

class Cat : public Animal<Cat> {
public:
    void run() override {
        std::cout << "Cat run" << std::endl;
    }
};

class Dog : public Animal<Dog> {
public:
    void run() override {
        std::cout << "Dog run" << std::endl;
    }
};

int main() {
    std::vector<BaseAnimal*> animalVec;
    animalVec.emplace_back(new Cat());
    // 报错了,因为vector存放的数据类型是Animal<Cat>
    animalVec.emplace_back(new Dog());
    for (auto animal: animalVec) {
        animal->run();
    }
    return 0;
}

这样一来,我们通过CRTP与虚函数结合,即保留了动态多态的各种特性,也减少了部分虚函数的查找开销。

推荐阅读

C++进阶系列

关注我,一起进步。

相关推荐
别NULL20 分钟前
机试题——最小矩阵宽度
c++·算法·矩阵
Icomi_1 小时前
【外文原版书阅读】《机器学习前置知识》1.线性代数的重要性,初识向量以及向量加法
c语言·c++·人工智能·深度学习·神经网络·机器学习·计算机视觉
apocelipes1 小时前
Linux glibc自带哈希表的用例及性能测试
c语言·c++·哈希表·linux编程
Ronin-Lotus2 小时前
上位机知识篇---CMake
c语言·c++·笔记·学习·跨平台·编译·cmake
wyg_0311133 小时前
C++资料
开发语言·c++
A charmer3 小时前
算法每日双题精讲 —— 二分查找(山脉数组的峰顶索引,寻找峰值)
c++·算法
Zfox_3 小时前
HTTP cookie 与 session
linux·服务器·网络·c++·网络协议·http
软工在逃男大学生3 小时前
转换算术表达式
c语言·数据结构·c++·算法
小黄人软件4 小时前
【MFC】C++所有控件随窗口大小全自动等比例缩放源码(控件内字体、列宽等未调整) 20250124
开发语言·c++·ui·mfc
兵哥工控4 小时前
MFC结构体数据文件读写实例
c++·mfc