尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Effective C++ 学习笔记 条款44 将与参数无关的代码抽离templates

Effective C++ 学习笔记 条款44 将与参数无关的代码抽离templates 模板是节省时间和避免代码重复的绝佳方式。你不需要编写20个相似的类每个类包含15个成员函数只需编写一个类模板然后让编译器为你实例化所需的20个特定类和300个函数即可类模板的成员函数只有在使用时才会被隐式实例化所以只有当你确实用到全部300个成员函数时它们才会被生成。函数模板也同样吸引人。你不需要编写许多函数只需编写一个函数模板剩下的交给编译器。技术可真了不起呀是的嗯……有时候吧。如果你不够小心使用模板可能会导致代码膨胀code bloat即二进制文件中出现了重复或几乎重复的代码、数据或两者皆有。结果可能是源码看起来精干利落但目标代码却臃肿肥硕。臃肿肥硕从来都不时髦所以你需要知道如何避免这种二进制文件的虚胖。你的主要工具叫做共性与变性分析commonality and variability analysis但这个概念本身并不高深。即使你一生中从未写过模板你其实也一直在做这样的分析。当你编写一个函数时如果意识到该函数的某部分实现与另一个函数的实现本质上相同你会直接复制代码吗当然不会。你会把公共代码从两个函数中提取出来放到第三个函数中然后让另外两个函数都去调用这个新函数。也就是说你分析这两个函数找出其中共有的部分和变化的部分把共有部分移到新函数中而把变化的部分留在原函数中。同样如果你在编写一个类发现类的某些部分与另一个类的部分相同你也不会复制那些共有部分。相反你会把共有部分移到新类中然后通过继承或组合参见条款32、38和39让原来的类能够使用这些共有功能。原类中那些不同的部分——即变化的部分——仍然留在原来的位置。在编写模板时你做的是同样的分析用同样的方式避免重复但有一个不同之处。在非模板代码中重复是显式的你可以看到两个函数或两个类之间存在重复。而在模板代码中重复是隐式的模板源码只有一份所以你必须训练自己去感知当模板被多次实例化时可能发生的重复。例如假设你要编写一个固定大小的方阵模板它除了其他功能外还支持矩阵求逆这段模板接受一个类型参数 T同时还接受一个 size_t 类型的参数——一个非类型参数。非类型参数不如类型参数常见但它们完全合法而且如本例所示它们可以非常自然。现在考虑这段代码这里将实例化两份 invert。这两个函数并不完全相同因为一个作用于 5×5 矩阵另一个作用于 10×10 矩阵但除了常量 5 和 10 之外两个函数是一样的。这是模板引发代码膨胀的典型方式。如果你看到两个函数除了一个用 5 而另一个用 10 之外逐字符相同你会怎么做你的直觉会是创建一个接受一个值作为参数的函数版本然后用 5 或 10 调用这个带参数的函数而不是复制代码。你的直觉是正确的下面是针对 SquareMatrix 做到这一点的初次尝试如你所见参数化的 invert 版本位于基类 SquareMatrixBase 中。与 SquareMatrix 一样SquareMatrixBase 是一个模板但与 SquareMatrix 不同的是它仅在矩阵中对象的类型上模板化而不是在矩阵的大小上。因此所有持有给定类型对象的矩阵将共享一个单一的 SquareMatrixBase 类。它们从而共享该类 invert 版本的一份副本。SquareMatrixBase::invert 的用途仅仅是作为派生类避免代码复制的一种方式所以它是 protected 而非 public。调用它的额外成本应该为零因为派生类的 invert 使用内联函数调用基类版本内联是隐式的——参见条款30。这些函数使用了 this- 标记法因为否则正如条款43所解释的模板化基类如SquareMatrixBaseT中的函数名称会被派生类隐藏。还请注意SquareMatrix 与 SquareMatrixBase 之间的继承是 private 的。这准确地反映了这样一个事实基类的存在仅仅是为了方便派生类的实现而不是为了表达 SquareMatrix 与 SquareMatrixBase 之间的概念上的 is-a 关系关于 private 继承的信息参见条款39。到目前为止一切顺利但我们还有一个棘手的问题没有解决。SquareMatrixBase::invert 如何知道要对什么数据进行操作它从参数中知道矩阵的大小但它如何知道特定矩阵的数据在哪里大概只有派生类知道。派生类如何将该信息传递给基类以便基类能够进行求逆运算呢给 SquareMatrixBase::invert 增加另一个参数是一种可能的方式也许是一个指向矩阵数据所在内存块起始位置的指针。这可行但很可能 invert 并不是 SquareMatrix 中唯一可以按与大小无关的方式编写并移入 SquareMatrixBase 的函数。如果有几个这样的函数它们都需要一种方法来找到存储矩阵值的内存。我们可以给它们都增加一个额外参数但这样我们会反复告诉 SquareMatrixBase 相同的信息。这似乎不太对。另一种方式是让 SquareMatrixBase 存储一个指向矩阵值内存的指针。既然它要存这个那不妨也把矩阵大小存下来。最终的设计如下这让派生类可以决定如何分配内存。有些实现可能会选择将矩阵数据直接存储在 SquareMatrix 对象内部这种类型的对象不需要动态内存分配但对象本身可能非常大。另一种选择是将每个矩阵的数据放在堆上无论数据存储在哪里从代码膨胀的角度来看关键结果是现在许多——也许是所有——SquareMatrix 的成员函数都可以简单地内联调用基类版本而这些基类版本与所有持有相同类型数据的其他矩阵无论其大小如何共享。同时不同大小的 SquareMatrix 对象是不同的类型所以即使SquareMatrixdouble, 5和SquareMatrixdouble, 10对象使用SquareMatrixBasedouble中相同的成员函数也绝不可能将一个SquareMatrixdouble, 5对象传递给一个期望SquareMatrixdouble, 10的函数。不错吧这段设计固然不错但也并非没有代价。将矩阵大小作为模板参数硬编码到函数中的 invert 版本很可能比共享版本大小作为函数参数传递或存储在对象中生成更好的代码。例如在尺寸特定的版本中大小是编译期常量因而可以进行常量传播等优化包括将常量作为立即数整合到生成的指令中。这在尺寸无关的版本中无法做到。另一方面为多种矩阵尺寸只保留一个 invert 版本能减小可执行文件的大小这可能降低程序的工作集working set大小并改善指令缓存的引用局部性。这些因素能使程序运行得更快其好处可能会超过尺寸特定版本中那些失去的优化所带来的损失。哪种效果会占主导唯一的方法是对两种方式进行测试并在你的特定平台和代表性数据集上观察行为。另一个效率考量涉及对象的大小。如果你不够小心将尺寸无关的函数版本上移到基类中可能会增加每个对象的总大小。例如在我刚才展示的代码中每个 SquareMatrix 对象在 SquareMatrixBase 类中都有一个指向其数据的指针即使每个派生类已经有办法访问数据。这使每个 SquareMatrix 对象至少增加了一个指针的大小。可以修改设计来避免这些指针但同样需要做出权衡。例如让基类存储一个指向矩阵数据的 protected 指针会导致条款22中所描述的封装性损失。它还可能导致资源管理复杂化如果基类存储了一个指向矩阵数据的指针但该数据可能是动态分配的也可能物理上存储在派生类对象内部如我们所见那么如何确定该指针是否应该被删除这些问题都有答案但你对它们考虑得越精细事情就越复杂。到了某个程度稍微复制一点代码反倒显得是一种仁慈了。本条款只讨论了由非类型模板参数引发的代码膨胀但类型参数同样可能导致代码膨胀。例如在许多平台上int 和 long 具有相同的二进制表示因此vectorint和vectorlong的成员函数很可能完全相同——这正是代码膨胀的定义。有些链接器会合并相同的函数实现但有些不会这意味着在某些环境中同时用 int 和 long 实例化的模板可能会造成代码膨胀。类似地在大多数平台上所有指针类型具有相同的二进制表示因此持有指针类型的模板如listint*、listconst int*、listSquareMatrixlong, 3*等通常应该能够为每个成员函数使用单一的底层实现。通常这意味着实现那些使用强类型指针即 T* 指针的成员函数时应该让它们去调用另一个无类型指针即 void* 指针的函数。标准 C 库的某些实现对于 vector、deque 和 list 等模板就是这样做的。如果你担心模板中的代码膨胀你很可能也想开发出做同样事情的模板。切记1.模板会生成多个类和多个函数因此任何不依赖于模板参数的模板代码都会导致代码膨胀。2.由于非类型模板参数导致的代码膨胀通常可以通过将模板参数替换为函数参数或类数据成员来消除。3.由于类型参数导致的代码膨胀可以通过为具有相同二进制表示的实例化类型共享实现来减少。
返回列表