读书笔记 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 消除代码膨胀第一关——去掉非类型参数

例如,假设你想为固定大小的矩阵实现一个模板,需要支持矩阵的转置。

1 template<typename T, // template for n x n matrices of
2 std::size_t n> // objects of type T; see below for info
3 class SquareMatrix { // on the size_t parameter
4 public:
5 ...
6
7 void invert();                         // invert the matrix in place
8
9 };     

这个模板带了一个类型参数,T,但是也带了一个类型size_t的参数,一个非类型(non-type)参数。非类型参数比类型参数少了共性,但是它们是完全合法的,并且在这个例子中,它们也能非常自然。

现在考虑下面的代码:

 1 SquareMatrix<double, 5> sm1;
 2
 3 ...
 4
 5 sm1.invert();
 6
 7 // call SquareMatrix<double, 5>::invert
 8
 9 SquareMatrix<double, 10> sm2;
10
11
12
13 ...
14
15
16
17 sm2.invert();
18
19 // call SquareMatrix<double, 10>::invert
20
21  

在这里将会实例化invert的两份拷贝。这两个函数并不相同,因为一个在5*5的矩阵上工作,另外一个在10*10的矩阵上工作,但是如果不考虑常量5和10,这两个函数将会是一样的。这是使得包含模板的代码出现膨胀的典型方式。

如果你看到两个函数,它们的所有字符都是相同的,除了一个版本使用5而另外一个版本使用10,你接下来会做什么?你的直觉是会创建一个带一个参数的函数版本,然后以5或者10为入参调用这个函数而不是重复代码。你的直觉能够很好的为你服务!这是实现SquareMatrix的第一关:

 1 template<typename T> // size-independent base class for
 2 class SquareMatrixBase { // square matrices
 3 protected:
 4 ...
 5 void invert(std::size_t matrixSize); // invert matrix of the given size
 6 ...
 7 };
 8 template<typename T, std::size_t n>
 9 class SquareMatrix: private SquareMatrixBase<T> {
10 private:
11 using SquareMatrixBase<T>::invert; // make base class version of invert
12 // visible in this class; see Items 3313 // and Item 43
14 public:
15 ...
16 void invert() { invert(n); } // make inline call to base class
17 }; // 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的。这精确的反映出一个事实:使用基类的唯一原因是帮助派生类的实现,并非表达出SquareMatrixSquareMatrixBase之间的“is-a”关系。(有关private继承的信息,见Item 39

2.2.2 消除代码膨胀第二关——派生类如何告知基类数据在哪里

到现在为止看上去都很好,但是还有一个我们没有处理的棘手的问题。SquareMatrixBase::invert如何知道在什么数据上进行操作?它从参数中得知矩形的大小,但是它如何知道为特殊矩阵提供的数据在哪里?大概只有派生类才会知道。派生类如何同基类进行通讯才能让基类执行invert?

一个可能的方法是向SquareMatrixBase::invert中添加另外一个参数,可能是一个指向一块内存的指针,内存中存放矩形数据。这种方法可以工作,但是十有八九,invert不是存在于SquareMatrix中的能够以独立于size的方式重写的,并且移入SquareMatrixBase中的唯一函数。如果有几个这样的函数,我们就需要一种方法能够找到存放矩形数据的内存,我们可以为所有的函数添加一个额外的参数,但是如此以来我们就重复告诉了SquareMatrixBase同样的信息。这看上去是错误的。

一个替换方法是让SquareMatrixBase存储一个指向存放矩形数据的内存的指针。这同存放矩形大小有相同的效果。结果如下:

 1 template<typename T>
 2 class SquareMatrixBase {
 3 protected:
 4 SquareMatrixBase(std::size_t n, T *pMem) // store matrix size and a
 5 : size(n), pData(pMem) {} // ptr to matrix values
 6
 7 void setDataPtr(T *ptr) { pData = ptr; }   // reassign pData
 8
 9 ...
10
11 private:
12
13
14
15 std::size_t size;           // size of matrix
16
17 T *pData;       // pointer to matrix values
18
19
20 };

这就让派生类来决定如何分配内存。一些实现会在SquareMatrix对象内部存储矩形数据:

1 template<typename T, std::size_t n>
2 class SquareMatrix: private SquareMatrixBase<T> {
3 public:
4 SquareMatrix() // send matrix size and
5 : SquareMatrixBase<T>(n, data) {} // data ptr to base class
6 ...
7 private:
8 T data[n*n];
9 };

这种类型的对象没有必要做动态内存分配,但是对象本身可能会非常大。一个替换的方法是为每个矩形在堆上存放数据:

 1 template<typename T, std::size_t n>
 2 class SquareMatrix: private SquareMatrixBase<T> {
 3 public:
 4 SquareMatrix() // set base class data ptr to null,
 5 : SquareMatrixBase<T>(n, 0), // allocate memory for matrix
 6 pData(new T[n*n]) // values, save a ptr to the
 7 { this->setDataPtr(pData.get()); } // memory, and give a copy of it
 8
 9 ...                                              // to the base class
10
11 private:
12 boost::scoped_array<T> pData;          // seeItem 13 for info on
13
14
15
16 };                                   // 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. 总结

  • 模板会产生多个类和多个函数,所以任何模板不应该依赖于会导致代码膨胀的模板参数。
  • 非类型模板参数导致的代码膨胀通常情况下可以将模板参数替换为函数参数或者类数据成员来清除。
  • 由类型参数导致的代码膨胀也可以被降低,方式是为实例化类型共享相同的二进制表示。
时间: 2024-10-18 04:23:14

读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来的相关文章

Effective C++ Item 44 将与參数无关的代码抽离 templates

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:Templates 生成多个 classes 和多个函数,所以不论什么 template 代码都不该与某个造成膨胀的 template 參数产生相依关系 因非类型模板參数(non-type template parameters) 而造成的代码膨胀,往往可消除,做法是以函数參数或 class 成员变量替换 template 參数 演示样例: template<typename T,

Effective C++:条款44:将与参数无关的代码抽离template

(一) template是节省时间和避免重复代码的一个奇妙方法.class template的成员函数只有在被使用时才被暗中具现化.function templates有类似的诉求. 但是如果你不小心,使用templates可能导致代码膨胀(code bloat):其二进制代码带着重复(或几乎重复)的代码.数据.或两者.其结果可能源码看起来合身整齐,但目标码却不是那么回事.你需要知道如何避免这样的二进制浮夸. 主要工具是:共性与变性分析. 在non-template中,重复十分明确,然而在tem

Effective C++ Item 44 将与参数无关的代码抽离 templates

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:Templates 生成多个 classes 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系 因非类型模板参数(non-type template parameters) 而造成的代码膨胀,往往可消除,做法是以函数参数或 class 成员变量替换 template 参数 示例: template<typename T, std:

读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)

最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热情和追求还是不减,应该是感动了周公吧,梦境从此处开始,大师入场来给我安慰了... 11点躺在床上了,脑子里总结一下最近的工作:最近的开发用到inline函数比较多,众所周知,inline的使用是为了提高程序性能,可结果却总不尽如人意,这个捉急啊,嗯?怎么突然到了山脚下,周边树木林立,郁郁葱葱,鸟儿委婉啼叫

读书笔记 effective c++ Item 55 让你自己熟悉Boost

你正在寻找一个高质量的,开源的,与平台和编译器无关的程序库的集合?看一下Boost吧.想加入一个由雄心勃勃的,充满天赋的正致力于最高水平的程序库设计和实现工作的C++程序员们组成的团体么?看一下Boost吧.想了解C++将来可能会是什么样子的?看一下Boost吧. Boost是一个C++开发人员组成的团体,也是供免费下载的C++程序库的集合.网址是http://boost.org. 1. Boost的两大优势 当然,有许多C++组织和网站,但是Boost有两点是其它组织不能与之媲美的.首先,它和

读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C++标准预计在2009年被发布(虽然所有的工作很有可能在2007年底被完成).直到现在,发布下一版C++的预计年份还没有被确定,这就解释了为什么人们把下一版C++叫做“C++0x”——C++的200x年版本. C++0x可能会包含一些有趣的新的语言特性,但是大多数新C++功能将会以标准库附加物的形式被

读书笔记 effective c++ Item 48 了解模板元编程

1. TMP是什么? 模板元编程(template metaprogramming TMP)是实现基于模板的C++程序的过程,它能够在编译期执行.你可以想一想:一个模板元程序是用C++实现的并且可以在C++编译器内部运行的一个程序,它的输出——从模板中实例化出来的C++源码片段——会像往常一样被编译. 2. 使用TMP的优势 如果这没有冲击到你,是因为你没有足够尽力去想. C++不是为了模板元编程而设计的,但是自从TMP早在1990年被发现之后,它就被证明是非常有用的,为了使TMP的使用更加容易

读书笔记 effective c++ Item 45 使用成员函数模板来接受“所有兼容类型”

智能指针的行为像是指针,但是没有提供加的功能.例如,Item 13中解释了如何使用标准auto_ptr和tr1::shared_ptr指针在正确的时间自动删除堆上的资源.STL容器中的迭代器基本上都是智能指针:当然,你不能通过使用“++”来将链表中的指向一个节点的内建指针移到下一个节点上去,但是list::iterator可以这么做. 1. 问题分析——如何实现智能指针的隐式转换 真正的指针能够做好的一件事情是支持隐式转换.派生类指针可以隐式转换为基类指针,指向非const的指针可以隐式转换成为

读书笔记 effective c++ Item 41 理解隐式接口和编译期多态

1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), 1 class Widget { 2 public: 3 Widget(); 4 virtual ~Widget(); 5 6 virtual std::size_t size() const; 7 virtual void normalize(); 8 9 void swap(Widget& other); // see Item 25 10 11 ... 12 13 }; 考虑下