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++进阶系列

关注我,一起进步。

相关推荐
hunandede20 分钟前
av_image_get_buffer_size 和 av_image_fill_arrays
c++
怀澈1222 小时前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
chnming19872 小时前
STL关联式容器之set
开发语言·c++
威桑2 小时前
MinGW 与 MSVC 的区别与联系及相关特性分析
c++·mingw·msvc
熬夜学编程的小王3 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list
yigan_Eins3 小时前
【数论】莫比乌斯函数及其反演
c++·经验分享·算法
Mr.133 小时前
什么是 C++ 中的初始化列表?它的作用是什么?初始化列表和在构造函数体内赋值有什么区别?
开发语言·c++
阿史大杯茶3 小时前
AtCoder Beginner Contest 381(ABCDEF 题)视频讲解
数据结构·c++·算法
C++忠实粉丝3 小时前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
时光の尘3 小时前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c