读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来
1. 使用模板可能导致代码膨胀
使用模板是节省时间和避免代码重用的很好的方法。你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定的类和300个你需要的函数。(只有在被使用的情况下类模版的成员函数才会被隐式的实例化,所以只有在300个函数被实际用到的情况下才会生成300个成员函数。)函数模板同样吸引人。你不用手动实现许多函数,你只需要实现一个函数模板,然后让编译器来做余下的事情。
然而在有些时候,如果你不小心,使用模板会导致代码膨胀(code bloat):产生重复代码或者数据的二进制文件,或者两者都有。结果可能是源码看起来合身整齐,但是目标代码(object code)臃肿松弛。臃肿松弛很不好,因此你需要知道如果避免这样的二进制浮夸。
2. 共性和可变性分析
你的主要工具有着很威风的名字:共性和可变性分析(commonality and variability analysis),但是这个概念很平常。即使在你的编程生涯中从未实现过一个模板,你也总是会做这样的分析。
2.1 函数和类中的代码重复分析
当你正在实现一个函数,你意识到函数实现的某些部分同另外一个函数实现基本上是相同的 ,你会重复这些代码么?当然不会。你将两个函数的公共代码提取出来,放进第三个函数中,然后在两个函数中调用这个新函数。总结一下就是,你对两个函数进行分析,找到相同和不同的部分,将相同的部分移到一个新的函数中去,将不同的部分保留在原来的函数中。类似的,如果你正在实现一个类,你意识到类中的一部分另一个类中的一部分是相同的,你不应该重写相同的部分。相反,你可以将相同的部分移到一个新类中,然后使用继承或者组合(Item 32,Item 38,Item 39)让原始类访问共同的特性。原始类中不同的部分仍然保留在原来的位置。
2.2 模板中的代码重复分析及消除重复方法
当实现模板的时候,你也会做相同的分析,你会使用相同的方式来阻止重复,但是这里有一个让你伤痛的地方。在非模板(non-template)代码中,重复是显示的:你可以看到在函数之间或者类之间会有代码重复。在模板代码中,重复是隐式的:只有一份模板源码,所以你必须训练你自己当一个模板被实例化多次的时候,你能够感觉到重复会不会发生。
2.2.1 消除代码膨胀第一关——去掉非类型参数
例如,假设你想为固定大小的矩阵实现一个模板,需要支持矩阵的转置。
template<typename T, // template for n x n matrices of
std::size_t n> // objects of type T; see below for info
class SquareMatrix { // on the size_t parameter
public:
... void invert(); // invert the matrix in place };
这个模板带了一个类型参数,T,但是也带了一个类型size_t的参数,一个非类型(non-type)参数。非类型参数比类型参数少了共性,但是它们是完全合法的,并且在这个例子中,它们也能非常自然。
现在考虑下面的代码:
SquareMatrix<double, > sm1; ... sm1.invert(); // call SquareMatrix<double, 5>::invert SquareMatrix<double, > sm2; ... sm2.invert(); // call SquareMatrix<double, 10>::invert
在这里将会实例化invert的两份拷贝。这两个函数并不相同,因为一个在5*5的矩阵上工作,另外一个在10*10的矩阵上工作,但是如果不考虑常量5和10,这两个函数将会是一样的。这是使得包含模板的代码出现膨胀的典型方式。
如果你看到两个函数,它们的所有字符都是相同的,除了一个版本使用5而另外一个版本使用10,你接下来会做什么?你的直觉是会创建一个带一个参数的函数版本,然后以5或者10为入参调用这个函数而不是重复代码。你的直觉能够很好的为你服务!这是实现SquareMatrix的第一关:
template<typename T> // size-independent base class for
class SquareMatrixBase { // square matrices
protected:
...
void invert(std::size_t matrixSize); // invert matrix of the given size
...
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>::invert; // make base class version of invert
// visible in this class; see Items 33 // and Item 43
public:
...
void invert() { invert(n); } // make inline call to base class
}; // version of invert
正如你所看到的,带参数的invert版本被放在基类SquareMatrixBase中。像SquareMatrix一样,SquareMatrixBase是一个模板,但是与SquareMatrix不同的是,它在矩阵中只对对象类型进行模板化。因此,包含一个给定类型对象的所有矩阵将会分享一个单一的SquareMatrixBase类。这样它们会分享SquareMatrixBase类的invert版本的单一拷贝。(你不能将其声明为inline,因为一旦被inline了,每个SquareMatrix::invert的实例都会得到SquareMatrixBase::invert代码的一份拷贝(看Item 30),你会发现你有回到了对象代码重复的原点。)
SquareMatrixBase::invert只被用来在派生类中防止代码重复,所以是protected而不是public的。调用它的额外开销应该是0,因为派生类的inverts调用基类版本使用了inline函数。(inline是隐式的 见Item 30)同时注意SquareMatrix和SquareMarixBase之间的继承是private的。这精确的反映出一个事实:使用基类的唯一原因是帮助派生类的实现,并非表达出SquareMatrix和SquareMatrixBase之间的“is-a”关系。(有关private继承的信息,见Item 39)
2.2.2 消除代码膨胀第二关——派生类如何告知基类数据在哪里
到现在为止看上去都很好,但是还有一个我们没有处理的棘手的问题。SquareMatrixBase::invert如何知道在什么数据上进行操作?它从参数中得知矩形的大小,但是它如何知道为特殊矩阵提供的数据在哪里?大概只有派生类才会知道。派生类如何同基类进行通讯才能让基类执行invert?
一个可能的方法是向SquareMatrixBase::invert中添加另外一个参数,可能是一个指向一块内存的指针,内存中存放矩形数据。这种方法可以工作,但是十有八九,invert不是存在于SquareMatrix中的能够以独立于size的方式重写的,并且移入SquareMatrixBase中的唯一函数。如果有几个这样的函数,我们就需要一种方法能够找到存放矩形数据的内存,我们可以为所有的函数添加一个额外的参数,但是如此以来我们就重复告诉了SquareMatrixBase同样的信息。这看上去是错误的。
一个替换方法是让SquareMatrixBase存储一个指向存放矩形数据的内存的指针。这同存放矩形大小有相同的效果。结果如下:
template<typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase(std::size_t n, T *pMem) // store matrix size and a
: size(n), pData(pMem) {} // ptr to matrix values void setDataPtr(T *ptr) { pData = ptr; } // reassign pData ... private: std::size_t size; // size of matrix T *pData; // pointer to matrix values };
这就让派生类来决定如何分配内存。一些实现会在SquareMatrix对象内部存储矩形数据:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() // send matrix size and
: SquareMatrixBase<T>(n, data) {} // data ptr to base class
...
private:
T data[n*n];
};
这种类型的对象没有必要做动态内存分配,但是对象本身可能会非常大。一个替换的方法是为每个矩形在堆上存放数据:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() // set base class data ptr to null,
: SquareMatrixBase<T>(n, ), // allocate memory for matrix
pData(new T[n*n]) // values, save a ptr to the
{ this->setDataPtr(pData.get()); } // memory, and give a copy of it ... // to the base class private:
boost::scoped_array<T> pData; // seeItem 13 for info on }; // boost::scoped_array
2.2.3 消除代码膨胀前后效率对比
不管将数据存放在哪里,从代码膨胀的角度来说,关键结果是现在很多(可能是所有的)SquareMatrix的成员函数可以简单的inline调用基类的(non-inline)函数版本,所有持有相同类型数据的矩形共享基类中的函数,不管size是多少。同时,不同size的SquareMatrix对象属于不同类型,所以即使SquareMatrix<double,5>和SquareMatrix<double,10>对象在SquareMatrixBase<double>中使用相同的成员函数,把一个SquareMatrix<double,5>对象传给一个需要SquareMatrix<double,10>的函数是没有机会的。好还是不好呢。
好是好,但是需要付出代价。矩形size大小固定的invert版本比按函数参数传递size大小(或者存储在对象中)的invert版本可能产生更好的代码。例如,在指定size的版本中,sizes是编译期常量,因此是常量传播优化的合格者,也可以把其放入生成指令中作为直接操作数。这在同size无关的版本中无法做到。
从另外一个方面,为不同size的矩阵只提供一个invert版本可以减小可执行程序的大小,这能减少程序的工作集大小,并且能够强化指令高速缓存的引用集中化。这些东西能够使得程序运行速度更快,并且相对size指定的版本才能做出的优化,它可能会做出更好的补偿。哪种方法效果更好?唯一的方法是两种方法都试一下,在你的特定平台和有代表性的数据集上观察它们的行为。
另外一个有关效率的需要考虑的地方是有关对象的大小。如果你不介意,将size大小无关的版本向上移动到基类中会增加每个对象的大小。例如,在我刚刚展示的代码中,每个SquareMatrix对象有一个指向SquareMatrixBase类中数据的指针。即使每个派生类中已经有取得数据的方法,这也为每个SquareMatrix对象至少增加一个指针的大小。我们可以修改设计来去掉指针,但是这也是需要付出代价的。例如,让基类存储一个指向数据的protected指针,但会导致封装性的降低(Item 22).它同样能导致资源管理并发症:如果基类存储了指向矩阵数据的指针,但是数据既有可能是动态分配的也可能存储在派生类对象中(正如我们看到的),如何决定是不是需要delete指针?这样的问题是有答案的,但是你做的越精细事情就变得越复杂。从某种意义上讲,有一点代码重复开始开起来有点幸运了。
2.3 如何处理类型模板参数导致的代码膨胀
这个条款仅仅讨论了由于非类型模板参数导致的代码膨胀,但是类型参数同样可以导致代码膨胀。例如,在许多平台中,int和long有着相同的二进制表示,所以在成员函数中使用vector<int>和vector<long>看起来会一样,这正是代码膨胀的定义。一些连接器会把相同的代码实现整合到一起,但是有一些不会,这就意味着由模板实例化的int和long版本会在一些环境中导致代码膨胀。类似的,在大多数平台上,所有的指针类型有着相同的二进制表示,所以带指针类型的模板(例如,list<int*>,list<const*>,list<SquareMatrix<long,3>*>等等)应该通常能够为每个成员函数使用一个单一的底层实现。特别的,这就意味着实现一个强类型指针(T* 指针)的成员函数时,让它们调用一个无类型指针的函数(void*指针)。一些标准C++库的实现为模板就是这么做的(如vector,deque,和list)。如果你关心在你的模板中出现的代码膨胀问题,你可能就会想开发出做相同事情的模板。
3. 总结
- 模板会产生多个类和多个函数,所以任何模板不应该依赖于会导致代码膨胀的模板参数。
- 非类型模板参数导致的代码膨胀通常情况下可以将模板参数替换为函数参数或者类数据成员来清除。
- 由类型参数导致的代码膨胀也可以被降低,方式是为实例化类型共享相同的二进制表示。
读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来的更多相关文章
- Effective C++ Item 44 将与參数无关的代码抽离 templates
本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:Templates 生成多个 classes 和多个函数,所以不论什么 templat ...
- Effective C++ -----条款44:将与参数无关的代码抽离templates
Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系. 因非类型模板参数(non-type template para ...
- 【44】将与参数无关的代码抽离templates
1.template是产生代码的代码,这就意味着源码看起来很少,生成的目标码大量膨胀. 2.考虑,如果两个方法有重复代码,我们会新建一个方法,把重复的代码放进去,原先两个方法调用第三个方法.如果两个类 ...
- 读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)
最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热情和追 ...
- 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库
1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...
- 读书笔记 effective c++ Item 55 让你自己熟悉Boost
你正在寻找一个高质量的,开源的,与平台和编译器无关的程序库的集合?看一下Boost吧.想加入一个由雄心勃勃的,充满天赋的正致力于最高水平的程序库设计和实现工作的C++程序员们组成的团体么?看一下Boo ...
- 读书笔记 effective c++ Item 48 了解模板元编程
1. TMP是什么? 模板元编程(template metaprogramming TMP)是实现基于模板的C++程序的过程,它能够在编译期执行.你可以想一想:一个模板元程序是用C++实现的并且可以在 ...
- 读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎
1. 自己实现一个资源管理类 Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如 ...
- 读书笔记 effective c++ Item 25 实现一个不抛出异常的swap
1. swap如此重要 Swap是一个非常有趣的函数,最初作为STL的一部分来介绍,它已然变成了异常安全编程的中流砥柱(Item 29),也是在拷贝中应对自我赋值的一种普通机制(Item 11).Sw ...
随机推荐
- JS特效——图片水平滚动
具体源码如下: <!doctype html> <html lang="en"> <head> <meta http-equiv=&quo ...
- 学习笔记——Java数字处理类
1.数字格式化 使用Java.text.DecimalFormat格式化数字,一般使用其中的DecimalFormat类.如: import java.text.DecimalFormat; publ ...
- ubuntu16.04下安装配置深度学习环境(一、cuda7.5的安装)
1.下载所需要的软件 cuda7.5下载(点击下载链接),cudnn4.0下载 2.安装NVIDIA驱动. 一般有两种方法:1)一种方法是利用"软件和更新"来安装,依次选择 系统设 ...
- USB重定向
第一期中,我们一起简要的看了下传统PC和桌面云下USB重定向和USB设备重定向方式的差异,了解了桌面云下外设兼容性问题来源的根源-USB设备本身驱动不规范/不支持, 或者虚拟机驱动实现上与USB设备对 ...
- windows container (docker) 容器资料笔记
背景 业务需求:简化公司私有云,公有云的部署,尝试寻找更好的,更优化的技术方案替换现有的虚拟机部署方案. 技术背景: .net Docker 学习资料 Docker中文社区: http://www.d ...
- Kubernetes运维生态-Heapster分析
Heapster在Kubernetes的运维生态中如下:集群的容器的监控数据收敛汇聚层 heapster1.0版本后内部分为event和metric两个进程,可制作为两个docker镜像部署为两个独立 ...
- GPU渲染管线概述
1.顶点着色器 顶点着色器是流水线的第一个阶段,它的输入来自于CPU.顶点着色器的处理单位是顶点,也就是说输入进来的每个顶点都会调用一次顶点着色器. 顶点着色器需要完成的工作主要有:坐标变换和逐顶点光 ...
- 联网html引用BootStrap
以下是我写的一个联网html引用BootStrap的例子,可作为参考: <%@ Page Language="C#" AutoEventWireup="true&q ...
- 《深入理解Java虚拟机》学习笔记之工具
善于利用工具,不仅可以加快我们分析数据,还可以快速定位和解决问题.现在我们就来看看虚拟机性能监控和故障处理工具. 在JDK的bin目录可以看到sun免费送给了我们很多小工具,这些工具虽然小巧但功能强大 ...
- Java使用Schema模式对XML验证
XML允许创作者定义自己的标签,因其灵活的特性让其难以编写和解析.因此必须使用某种模式来约束其结构.目前最流行的这种模式有两种:DTD和SCHEMA,而后者以其独特的优势即将取代DTD模式,目前只是过 ...