C++惯用法之CRTP(奇异递归模板模式)

2024-02-24 23:04

本文主要是介绍C++惯用法之CRTP(奇异递归模板模式),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

相关系列文章

C++惯用法之Pimpl

C++之数据转换(全)

目录

1.介绍

2.CRTP的使用场景

2.1.实现静态多态

2.2.代码复用和扩展性

3.总结


1.介绍

        CRTP的全称是Curiously Recurring Template Pattern,即奇异递归模板模式,简称CRTP。CRTP是一种特殊的模板技术和使用方式,是C++模板编程中的一种惯用法。基本特征表现为:基类是一个模板类;派生类在继承该基类时,将派生类自身作为模板参数传递给基类。下面用网上的实例来说明为什么要用CRTP? 比如我要实现一个数学库,如果使用运行时多态来实现向量类Vector,那么代码结构大致如下:

template<typename Type, unsigned Len>
struct VectorBase{virtual void someFunction(){...}...
};
struct Vector3: VectorBase<float, 3>{virtual void someFunction() override {...}
};

         需要注意的是,运行时多态是有开销的,熟悉c++虚函数的人应该就能明白,如果我调用一个虚函数,需要查询对象头部的虚函数表来得到实际函数地址,这个寻址的开销对于一个数学库而言是非常巨大的。而如果使用静态多态,则可以使用如下的代码来实现:

template <typename ChildType> struct VectorBase {ChildType &underlying() { return static_cast<ChildType &>(*this); }inline ChildType &operator+=(const ChildType &rhs) {this->underlying() = this->underlying() + rhs;return this->underlying();}
};
struct Vec3f : public VectorBase<Vec3f> {float x{}, y{}, z{};Vec3f() = default;Vec3f(float x, float y, float z) : x(x), y(y), z(z) {}
};inline Vec3f operator+(const Vec3f &lhs, const Vec3f &rhs) {Vec3f result;result.x = lhs.x + rhs.x;result.y = lhs.y + rhs.y;result.z = lhs.z + rhs.z;return result;
}

        可以看到,静态多态虽然导致代码复用度相较于运行时多态降低了很多,但是相较于完全手写,我们可以利用子类实现的operator+来通过CRTP自动添加operator+=,相当于是做到了运行效率与开发效率的相对平衡。 

        VectorBase是模版基类,派生类Vec3f 继承自VectorBase,并以自身作为模板参数传递给基类, 在基类内部,通过使用static_cast,将this指针转换为模板参数类型T的指针,然后调用类型T的方法imp。static_cast的用法可参考一下博客。

C++之数据转换(全)_c++数据类型转换-CSDN博客

2.CRTP的使用场景

2.1.实现静态多态

C ++支持动态和静态多态。

  • 动态多态性:在这种类型的多态性中,在编译时不知道对象的类型(可能基于用户输入等),因此编译器添加了额外的数据结构来支持这一点。 该标准并未规定应如何实施。C++通过虚函数表实现多态,但是虚函数会影响类的内存布局,并且虚函数的调用会增加运行时的开销。
  • 静态多态性:在这种类型中,对象的类型在编译时本身是已知的,因此实际上无需在数据结构中保存额外的信息。 但是如前所述,我们需要在编译时知道对象的类型。

CRTP 可以实现静态多态,但本质上 CRTP 中的多个派生类并不是同一个基类,因此严格意义上不能叫多态。示例如下:

template<typename T>
class base
{
public:virtual ~base(){}void interface() { static_cast<T*>(this)->impl(); }void impl() { cout << "base impl... " << endl; }
};class derived1 : public base<derived1>
{
public:void impl(){ cout << "derived1 impl... " << endl; }
};class derived2 : public base<derived2>
{
public:void impl() { cout << "derived2 impl..." << endl; }
};template<typename T>
void testDemo(T & base)
{base.interface();
}int main()
{derived1 a1;derived2 a2;testDemo(a1);  //输出:derived1 impl... testDemo(a2);  //输出:derived2 impl... return 0;
}

2.2.代码复用和扩展性

使用 CRTP 可以把重复性的代码抽象到基类中,减少代码冗余。示例代码如下:

template<typename T>
class base
{
public:virtual ~baseDemo(){}void getType() { T& t = static_cast<T&>(*this);cout << typeid(t).name() << endl;} 
};class derived1 : public base<derived1>
{
};class derived2 : public base<derived2>
{
};int main()
{derived1  a1;derived2  a2;a1.getType(); //输出:class derived1a2.getType(); //输出:class derived2return 0;
}

        可以看到,在基类中getType函数中能够获取到派生于base所有类的类型信息,相比于虚函数的方式减少了代码。上面的代码在getType函数不变的情况下,可以任意编写base的扩展类,提高了代码的可重用性和灵活性。

        我们再看一个示例:多态拷贝构造(Polymorphic copy construction)

// Base class has a pure virtual function for cloning
class Shape {
public:virtual ~Shape() {};virtual Shape *clone() const = 0;
};
// This CRTP class implements clone() for Derived
template <typename Derived>
class Shape_CRTP : public Shape {
public:virtual Shape *clone() const {return new Derived(static_cast<Derived const&>(*this));}
};// Nice macro which ensures correct CRTP usage
#define Derive_Shape_CRTP(Type) class Type: public Shape_CRTP<Type>// Every derived class inherits from Shape_CRTP instead of Shape
Derive_Shape_CRTP(Square) {};
Derive_Shape_CRTP(Circle) {};

         传统的实现方式是,基类有个虚拟clone函数,每个继承类实现自己的clone函数功能。依靠CRTP技术,只定义一个就够了,大家通用,一样减少了冗余代码,提高了代码的复用性。

3.总结

  • 优点:省去动态绑定、查询虚函数表带来的开销。通过CRTP,基类可以获得到派生类的类型,提供各种操作,比普通的继承更加灵活。但CRTP基类并不会单独使用,只是作为一个模板的功能。
  • 缺点:使用CRTP需要编写更多的模板代码,增加了代码的复杂度,对于不熟悉模板编程的开发者来说可能会带来一定的学习成本。

这篇关于C++惯用法之CRTP(奇异递归模板模式)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/743647

相关文章

IDEA自动生成注释模板的配置教程

《IDEA自动生成注释模板的配置教程》本文介绍了如何在IntelliJIDEA中配置类和方法的注释模板,包括自动生成项目名称、包名、日期和时间等内容,以及如何定制参数和返回值的注释格式,需要的朋友可以... 目录项目场景配置方法类注释模板定义类开头的注释步骤类注释效果方法注释模板定义方法开头的注释步骤方法注

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(