Effective_C++_条款四十四:将与参数无关的代码抽离template

2024-01-20 20:38

本文主要是介绍Effective_C++_条款四十四:将与参数无关的代码抽离template,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

标题上说“将与参数无关的代码抽离template”,这里的参数既可以指类型,也可以是非类型,我们先来看看非类型的情况。

假定我们要为矩阵写一个类,这个矩阵的行列元素个数相等,是一个方阵,因而我们可以对之求逆运算。因为方阵的元素可以有多种类型,同时方阵的维数(方阵大小)也可以不同,像下面这样,我们使用了模板:

template <class T, size_t n>
class SquareMatrix
{
public:void Invert();
};int main()
{SquareMatrix<int, 10> a;SquareMatrix<int, 5> b;
}

模板既可以指定类型,也可以指定其他参量,比如这里的size_t,用来表示方阵的维数。程序看起来是没有问题的,编译也是通过的,但从代码优化上看,还是有空间可以做的。a和b虽然都是元素为int型的方阵,但因为方阵维数不同,因而生成了两个Invert函数,这两个Invert函数的代码只是在循环个数上不同(取觉于方阵维数n),但算法思想完全是一样的。

为了防止编译器生成冗余的代码,我们可以将Invert抽离这里,变成一个以n为参数的函数,像下面这样:

class SquareMatrixBase
{
public:SquareMatrixBase(T* p) : DataPointer(p){}void Invert(size_t n){}
private:T* DataPointer;
};template <class T, size_t n>
class SquareMatrix: private SquareMatrixBase<T>//private 继承
{
public:SquareMatrix() : SquareMatrixBase(Data){}void Invert(){SquareMatrixBase::Invert(n);//调父类函数}
private:T Data[n * n];
};

这里我们定义了一个BaseSquareMatrix类,这个类可以视为一个工具类,因为SquareMatrix是private继承于它的,我们把算法一致、只与维数n相关的算法函数都放在BaseSquareMatrix中,且将n作为它的Invert()函数的形参,为了使BaseSquareMatrix可以访问数据,从而求逆运算,我们声明了它的成员指针,这个指针将会在构造时指向子类的数据存储空间。

子类SquareMatrix没有大变化,只是在Invert的时候,用了一句代码就搞定了——调用父类的Invert函数,并把维数n传过去。

这样在main函数中,针对类型都是int,但矩阵维数不同的情况,Invert中生成的冗余代码非常少(只有一句话),父类也因为模板参数与维数无关,所以也只生成一份。从这个角度来看,将与参数无关的代码抽离template起到了减少代码冗余度的优化作用。(???但是在子类中依然有size 模板参数)

 

目前只是说将与类型无关的代码进行抽离(造一个父类,把算法相同的部分提取出来,放到父类),对于不同类型,其实也可以通过void*来实。比如STL,要想令我们使用list<int*>,list<const int*>生成的代码冗余度保持很低,这就需要在相应的函数里面,将之链到父类的一个公共实现版本里面,这个版本的形参是void*。

 

最后总结一下:

1. Template生成多个classes与多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。

2. 因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或者class成员变量替换template参数。

3. 因类型而造成的代码膨胀,也可以降低,做法是让带有完全相同二进制表述的具现类型共享实现码。

 

  • 模板是个好东西,你可以在保证类型安全的同时用一份代码同时兼容不同类型。 但模板提供的是编译期的多态, 即使你的代码看起来非常简洁短小,生成的二进制文件也可能包含大量的冗余代码。 因为模板每次实例化都会生成一个完整的副本,所以其中与模板参数无关的部分会造成代码膨胀(code bloat)。

把模板中参数无关的代码重构到模板外便可以有效地控制模板产生的代码膨胀。 另外代码膨胀也可以由类型模板参数产生:

  • 对于非类型模板参数产生的代码膨胀,用函数参数或数据成员来代替模板参数即可消除冗余;
  • 对于类型模板参数产生的代码膨胀,可以让不同实例化的模板类共用同样的二进制表示。

抽取公共代码

在避免代码冗余的问题上,抽取公共代码(commonality and variability analysis)是我们每天都在用的方法。 当你写几个函数时,会把其中的公共部分抽取到另一个函数;当你声明类时,也会把它们的公共部分抽取到父类中。

于是你希望在模板编程中也用该办法来避免代码重复,但模板和非模板代码在这一点上是不同的:

  • 非模板的代码中,冗余的显式的(explicit)。只要有重复代码你都会看到它;
  • 模板代码中,冗余是隐式的(implicit)。模板代码只有一份,模板被实例化时产生的冗余需要你的直觉才能感受到。

模板产生的代码膨胀

现在来看一个模板是怎样引发代码膨胀的。比如要实现一个固定大小的矩阵,它支持转置运算。

template<typename T, int n>
class Square{
public:void invert();
};

其中的int n是一个非类型参数,它也是一种合法的模板参数~

然后可能会这样使用该模板:

Square<double, 5> s1;
Square<double, 10> s2;
s1.invert();    s2.invert();

Square模板会实例化两个类:Square<double, 5>Square<double, 10>,它们拥有相同的invert方法。 这是模板产生代码膨胀的典型场景。

抽取父类模板

结局模板产生的代码膨胀,仍然是用抽取公共代码的办法。如果你真的看到了二进制代码中两个相同的invert函数, 你的直觉肯定是把它抽取到另一个类中:

template<typename T>
class SquareBase{
protected:void invert(int size);
};template<typename T, int n>
class Square:private SquareBase<T>{
private:using SquareBase<T>::invert;
public:void invert(){ this->invert(n); }
}

因为invert函数定义在基类中,所以它只会在二进制代码中出现一次,即SquareBase<double>::invert。该函数由两个子类共享。 上述代码中有些细节还值得一提:

  • SquareBase::invert是供子类用的,所以声明为private而不是public
  • 调用父类invert的代价为零,因为Square::invert是隐式的inline函数,见Item 30;
  • 使用this->前缀是因为,SquareBase里的名称在子类模板Square里是隐藏的,见Item 43;
  • 使用private继承是因为,Square is implemented in terms of Square,见Item 39。

数据存储问题

既然我们决定由父类来做invert操作,那么父类怎么访问数据呢?因为数据本来是在子类中的。 当然我们可以在调用SquareBase::invert时把内存地址也一起告知父类, 但如果矩阵类中有很多函数都需要这些信息呢?我们可能需要调用每个函数时都把这些信息传递给父类函数。 既然这样,何不把数据地址直接放在父类中?既然父类存放了数据,那么就把矩阵大小也一并存放了吧!

template<typename T>
class SquareBase{
protected:SquareBase(int _n, T *_data): n(_n), data(_data){}void setData(T *_data){data = _data;}
private:int n;T* data;
};

父类中存储了矩阵数据的位置(data)以及大小(n),子类仍然可以决定如何分配地址空间。 可以存放在子类中作为成员属性,也可以动态申请内存。

权衡

不管数据是怎样分配和访问的,我们消除代码重复的方案是确定的:将公共部分抽取到父模板类中。 这样做的好处便是避免了代码膨胀,减小了二进制文件和”working set”的大小,有利于提高指令缓存的命中率, 从而达到更高的代码执行效率。但提取公共部分到新的模板类也造成了一些问题:

  • 如果int n硬编码在模板参数中的话,编译器能做更多的优化,比如常量传播等。但int n作为函数参数,这些优化就没有了。
  • 添加类的层级会导致对象大小的增加。至少多存储了一个T* data指针。

实践中到底是否应该抽取公共代码出来取决于你的应用场景,在上述的优劣中进行权衡。

本问讨论的是非类型模板参数,对于类型模板参数,代码膨胀的问题也是存在的,比如

  • intlong在多数平台都是一样的底层实现,然而模板却会实例化为两份,因为它们类型不同。
  • List<int *>List<const int *>List<double *>的底层实现也是一样的。但因为指针类型不同,也会实例化为多份模板类。

 

这篇关于Effective_C++_条款四十四:将与参数无关的代码抽离template的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

java之Objects.nonNull用法代码解读

《java之Objects.nonNull用法代码解读》:本文主要介绍java之Objects.nonNull用法代码,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录Java之Objects.nonwww.chinasem.cnNull用法代码Objects.nonN

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

python+opencv处理颜色之将目标颜色转换实例代码

《python+opencv处理颜色之将目标颜色转换实例代码》OpenCV是一个的跨平台计算机视觉库,可以运行在Linux、Windows和MacOS操作系统上,:本文主要介绍python+ope... 目录下面是代码+ 效果 + 解释转HSV: 关于颜色总是要转HSV的掩膜再标注总结 目标:将红色的部分滤

在C#中调用Python代码的两种实现方式

《在C#中调用Python代码的两种实现方式》:本文主要介绍在C#中调用Python代码的两种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#调用python代码的方式1. 使用 Python.NET2. 使用外部进程调用 Python 脚本总结C#调

C++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

Java中&和&&以及|和||的区别、应用场景和代码示例

《Java中&和&&以及|和||的区别、应用场景和代码示例》:本文主要介绍Java中的逻辑运算符&、&&、|和||的区别,包括它们在布尔和整数类型上的应用,文中通过代码介绍的非常详细,需要的朋友可... 目录前言1. & 和 &&代码示例2. | 和 ||代码示例3. 为什么要使用 & 和 | 而不是总是使

一文带你了解SpringBoot中启动参数的各种用法

《一文带你了解SpringBoot中启动参数的各种用法》在使用SpringBoot开发应用时,我们通常需要根据不同的环境或特定需求调整启动参数,那么,SpringBoot提供了哪些方式来配置这些启动参... 目录一、启动参数的常见传递方式二、通过命令行参数传递启动参数三、使用 application.pro

Java强制转化示例代码详解

《Java强制转化示例代码详解》:本文主要介绍Java编程语言中的类型转换,包括基本类型之间的强制类型转换和引用类型的强制类型转换,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录引入基本类型强制转换1.数字之间2.数字字符之间引入引用类型的强制转换总结引入在Java编程语言中,类型转换(无论