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

关注我,一起进步。

相关推荐
Languorous.几秒前
C++数据结构进阶|堆(Heap)详解:从手写实现到面试高频实战
数据结构·c++·面试
khalil102010 分钟前
代码随想录算法训练营Day-49 图论01 | 图论理论基础、深搜理论基础、98. 所有可达路径、广搜理论基础
c++·算法·leetcode·深度优先·图论
Cinema KI26 分钟前
Linux C/C++ 编译构建:GCC/G++ + Makefile 零基础完整教程
linux·c语言·c++
念恒1230643 分钟前
基础IO(文件缓冲区)
linux·c语言·c++
君义_noip1 小时前
CSP-S 2025 提高级 第一轮(初赛) 阅读程序(3)
c++·算法·信息学奥赛·csp-s 初赛
汉克老师1 小时前
GESP6级C++考试语法知识(三、图与树(三))
c++·中序遍历·bst·完全二叉树·二叉排序树·gesp6级·gesp六级
啊罗罗1 小时前
windows下,c++的axv2+fma/avx-vnni加速计算demo
c++·windows·算法
li星野1 小时前
滑动窗口五题通关:从最小覆盖子串到水果成篮(Python + C++)
c++·python·学习
Sylvia33.1 小时前
足球数据API接入实战:从认证到实时比分推送的完整指南
java·开发语言·前端·c++·python
时空自由民.2 小时前
C/C++ volatile关键字原理及应用介绍
java·c语言·c++