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

相关文章

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

SpringBoot 获取请求参数的常用注解及用法

《SpringBoot获取请求参数的常用注解及用法》SpringBoot通过@RequestParam、@PathVariable等注解支持从HTTP请求中获取参数,涵盖查询、路径、请求体、头、C... 目录SpringBoot 提供了多种注解来方便地从 HTTP 请求中获取参数以下是主要的注解及其用法:1

HTTP 与 SpringBoot 参数提交与接收协议方式

《HTTP与SpringBoot参数提交与接收协议方式》HTTP参数提交方式包括URL查询、表单、JSON/XML、路径变量、头部、Cookie、GraphQL、WebSocket和SSE,依据... 目录HTTP 协议支持多种参数提交方式,主要取决于请求方法(Method)和内容类型(Content-Ty

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

Python 基于http.server模块实现简单http服务的代码举例

《Python基于http.server模块实现简单http服务的代码举例》Pythonhttp.server模块通过继承BaseHTTPRequestHandler处理HTTP请求,使用Threa... 目录测试环境代码实现相关介绍模块简介类及相关函数简介参考链接测试环境win11专业版python

Python从Word文档中提取图片并生成PPT的操作代码

《Python从Word文档中提取图片并生成PPT的操作代码》在日常办公场景中,我们经常需要从Word文档中提取图片,并将这些图片整理到PowerPoint幻灯片中,手动完成这一任务既耗时又容易出错,... 目录引言背景与需求解决方案概述代码解析代码核心逻辑说明总结引言在日常办公场景中,我们经常需要从 W