effective c++条款44 将与参数无关的代码抽离templates

首先了解这个条款的含义:使用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++条款44 将与参数无关的代码抽离templates相关推荐

  1. Effective C++ 条款44

    本节条款的标题是:将与參数无关的代码抽离templates 学习本节条款首先须要明确一件事情,那就是模板实例化的过程会不会反复? 我们来举个样例: #include<iostream> u ...

  2. Effective C++条款(第三版-侯杰译)

    条款一:视C++为一个语言联邦 [C++高效编程守则视情况而变化,取决于你使用的C++哪一部分] 条款二:尽量以const,enum,inline替换#define [对于单纯变量,最好以const对 ...

  3. Effective C++条款粗略总结

    文章目录 Effective C++ 1.类/结构体 2.资源管理 3.实现 4.模板与泛型编程 5.定制new和delete 6.其他 Effective C++ 1.类/结构体 1.把C++看成一 ...

  4. effective c++条款11扩展——关于拷贝构造函数和赋值运算符

    effective c++条款11扩展--关于拷贝构造函数和赋值运算符 作者:冯明德 重点:包含动态分配成员的类 应提供拷贝构造函数,并重载"="赋值操作符. 以下讨论中将用到的例 ...

  5. Effective C++条款09:绝不在构造和析构过程中调用virtual函数

    Effective C++条款09:绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruc ...

  6. Effective C++条款05:了解C++默默编写并调用哪些函数(Know what functions C++ silently writes and calls)

    Effective C++条款05:了解C++默默编写并调用哪些函数(Know what functions C++ silently writes and calls) 条款05:了解C++默默编写 ...

  7. Effective C++条款20:宁以pass-by-reference-to-const替换pass-by-value

    Effective C++条款20:宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to p ...

  8. Effective C++条款30:透彻了解inlining的里里外外(Understand the ins and outs of inlining)

    Effective C++条款30:透彻了解inlining的里里外外(Understand the ins and outs of inlining) 条款30:透彻了解inlining的里里外外 ...

  9. Perturbed Masking:和参数无关的预训练模型分析方法

    ©PaperWeekly 原创 · 作者|蔡杰 学校|北京大学硕士生 研究方向|问答系统 论文标题: Perturbed Masking: Parameter-free Probing for Ana ...

最新文章

  1. api restful_HATEOAS的RESTful服务。 记录超媒体API
  2. 谁都能看懂的网络模型知识
  3. php chr 乱码,php chr() ord()中文截取乱码问题解决方法_PHP教程
  4. springboot的api_【粉丝投稿】无需额外注解的 SpringBoot API文档生成工具
  5. 在Eclipse中手动安装pydev插件
  6. 开机即启动Activity
  7. Tosca 给定义变量,取内容放到变量里
  8. RDS for MySQL 物理备份文件恢复到本地数据库(亲测)
  9. Java解析魔兽争霸3录像W3G文件(二):压缩数据块解压合并
  10. 天堂2单机启动不了mysql_天堂2第8章单机版启动问题
  11. codeforces竞赛1141题解
  12. Android RSA加密解密
  13. Android 使用Alarm机制创建长时间在后台运行的定时任务
  14. 停不下的脚步:IT高管人士的工作实录
  15. gallary滑动切换图片
  16. 联想小新使用Alt+insert快捷键
  17. STM32使用SPI方式读写SD 卡
  18. 《大数据时代(BIG DATA)》
  19. 微服务之API网关:Kong:概要与安装
  20. dell服务器 指示灯_Dell PowerEdge:传统服务器(第10代和第11代)上的系统LED状态指示灯...

热门文章

  1. 计算机组成原理里的QD键,计算机组成原理—微程序控制器组成实验.docx
  2. 了解中国的组织结构后续
  3. 在第一次使用德国小鸡要注意的地方
  4. 攻击CSK靶机环境并学习linux渗透思路
  5. 全网首创Python影视剧解说视频文字自动剪辑第1版
  6. 五十岁:人生的另一种透彻
  7. valleyview gen7 linux驱动,天敏睿盒iBox win8.1界面大曝光!!
  8. 分布式算力新时代下 看安超云ArSDN如何保障云边协同
  9. python中字典的循环遍历的两种方式
  10. Yolov5环境配置 配不好来打我