news 2026/8/24 14:11:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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所解释的,模板化基类(如SquareMatrixBase<T>)中的函数名称会被派生类隐藏。还请注意,SquareMatrix 与 SquareMatrixBase 之间的继承是 private 的。这准确地反映了这样一个事实:基类的存在仅仅是为了方便派生类的实现,而不是为了表达 SquareMatrix 与 SquareMatrixBase 之间的概念上的 is-a 关系(关于 private 继承的信息,参见条款39)。

到目前为止,一切顺利,但我们还有一个棘手的问题没有解决。SquareMatrixBase::invert 如何知道要对什么数据进行操作?它从参数中知道矩阵的大小,但它如何知道特定矩阵的数据在哪里?大概只有派生类知道。派生类如何将该信息传递给基类,以便基类能够进行求逆运算呢?

给 SquareMatrixBase::invert 增加另一个参数是一种可能的方式,也许是一个指向矩阵数据所在内存块起始位置的指针。这可行,但很可能 invert 并不是 SquareMatrix 中唯一可以按与大小无关的方式编写并移入 SquareMatrixBase 的函数。如果有几个这样的函数,它们都需要一种方法来找到存储矩阵值的内存。我们可以给它们都增加一个额外参数,但这样我们会反复告诉 SquareMatrixBase 相同的信息。这似乎不太对。

另一种方式是让 SquareMatrixBase 存储一个指向矩阵值内存的指针。既然它要存这个,那不妨也把矩阵大小存下来。最终的设计如下:

这让派生类可以决定如何分配内存。有些实现可能会选择将矩阵数据直接存储在 SquareMatrix 对象内部:

这种类型的对象不需要动态内存分配,但对象本身可能非常大。另一种选择是将每个矩阵的数据放在堆上:

无论数据存储在哪里,从代码膨胀的角度来看,关键结果是:现在许多——也许是所有——SquareMatrix 的成员函数都可以简单地内联调用基类版本,而这些基类版本与所有持有相同类型数据的其他矩阵(无论其大小如何)共享。同时,不同大小的 SquareMatrix 对象是不同的类型,所以即使SquareMatrix<double, 5>SquareMatrix<double, 10>对象使用SquareMatrixBase<double>中相同的成员函数,也绝不可能将一个SquareMatrix<double, 5>对象传递给一个期望SquareMatrix<double, 10>的函数。不错吧?

这段设计固然不错,但也并非没有代价。将矩阵大小作为模板参数硬编码到函数中的 invert 版本,很可能比共享版本(大小作为函数参数传递或存储在对象中)生成更好的代码。例如,在尺寸特定的版本中,大小是编译期常量,因而可以进行常量传播等优化,包括将常量作为立即数整合到生成的指令中。这在尺寸无关的版本中无法做到。

另一方面,为多种矩阵尺寸只保留一个 invert 版本能减小可执行文件的大小,这可能降低程序的工作集(working set)大小,并改善指令缓存的引用局部性。这些因素能使程序运行得更快,其好处可能会超过尺寸特定版本中那些失去的优化所带来的损失。哪种效果会占主导?唯一的方法是对两种方式进行测试,并在你的特定平台和代表性数据集上观察行为。

另一个效率考量涉及对象的大小。如果你不够小心,将尺寸无关的函数版本上移到基类中可能会增加每个对象的总大小。例如,在我刚才展示的代码中,每个 SquareMatrix 对象在 SquareMatrixBase 类中都有一个指向其数据的指针,即使每个派生类已经有办法访问数据。这使每个 SquareMatrix 对象至少增加了一个指针的大小。可以修改设计来避免这些指针,但同样,需要做出权衡。例如,让基类存储一个指向矩阵数据的 protected 指针会导致条款22中所描述的封装性损失。它还可能导致资源管理复杂化:如果基类存储了一个指向矩阵数据的指针,但该数据可能是动态分配的,也可能物理上存储在派生类对象内部(如我们所见),那么如何确定该指针是否应该被删除?这些问题都有答案,但你对它们考虑得越精细,事情就越复杂。到了某个程度,稍微复制一点代码反倒显得是一种仁慈了。

本条款只讨论了由非类型模板参数引发的代码膨胀,但类型参数同样可能导致代码膨胀。例如,在许多平台上,int 和 long 具有相同的二进制表示,因此vector<int>vector<long>的成员函数很可能完全相同——这正是代码膨胀的定义。有些链接器会合并相同的函数实现,但有些不会,这意味着在某些环境中,同时用 int 和 long 实例化的模板可能会造成代码膨胀。类似地,在大多数平台上,所有指针类型具有相同的二进制表示,因此持有指针类型的模板(如list<int*>list<const int*>list<SquareMatrix<long, 3>*>等)通常应该能够为每个成员函数使用单一的底层实现。通常,这意味着实现那些使用强类型指针(即 T* 指针)的成员函数时,应该让它们去调用另一个无类型指针(即 void* 指针)的函数。标准 C++ 库的某些实现对于 vector、deque 和 list 等模板就是这样做的。如果你担心模板中的代码膨胀,你很可能也想开发出做同样事情的模板。

切记:
1.模板会生成多个类和多个函数,因此任何不依赖于模板参数的模板代码都会导致代码膨胀。

2.由于非类型模板参数导致的代码膨胀,通常可以通过将模板参数替换为函数参数或类数据成员来消除。

3.由于类型参数导致的代码膨胀,可以通过为具有相同二进制表示的实例化类型共享实现来减少。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/24 14:09:34

法系高定正红哑光源头工厂:不靠logo撑门面,靠的是这三道硬指标

说句大实话&#xff0c;这两年来厂里对接的实体店主和私域团长&#xff0c;十个里有七个是从“法系头部D家经典正红哑光”这个单品切入做私域信任感的。但很多人被上游批发商一顿忽悠&#xff0c;拿回去的货要么上嘴发玫发橘&#xff0c;要么拔干到唇纹能夹死蚊子&#xff0c;客…

作者头像 李华
网站建设 2026/8/24 14:06:58

sunshine - ubuntu 22.04, moonlight - mac OS26 远程桌面

1. 安装 https://github.com/LizardByte/Sunshine/releases 下载最新版&#xff0c; wget https://github.com/LizardByte/Sunshine/releases/download/v2026.817.185037/sunshine-ubuntu-22.04-amd64.debsudo apt install sunshine-ubuntu-22.04-amd64.deb以下是 Ubuntu 22.04…

作者头像 李华
网站建设 2026/8/24 14:05:28

SAP Gateway Foundation OData V4 服务管理全景解析,从 /IWBEP/V4_ADMIN 到 Service Group 的完整生命周期

在一个 SAP S/4HANA 后端系统里,ABAP 开发已经完成,Model Provider Class 和 Data Provider Class 也都能正常激活,到了准备把 OData V4 服务交给 SAP Fiori、外部集成平台或者其他系统调用的时候,真正决定这个服务能不能进入运行态的,并不只是那些 ABAP Class。 此时会进…

作者头像 李华
网站建设 2026/8/24 14:04:45

Linux系统之网络基础概念

本篇目标&#xff1a;1.了解网络传输基本流程2.认识MAC&#xff0c;IP地址3.了解数据包封装和分用过程一.网络传输基本流程1.局域网通信原理1.1.举例说明本次以局域网中的以太网为例子&#xff0c;我们先想一个问题&#xff1a;两台主机在同⼀个局域网&#xff0c;是否能够直接…

作者头像 李华
网站建设 2026/8/24 14:03:46

JeecgBoot3.8.3信创环境深度部署实战

JeecgBoot3.8.3信创环境深度部署实战本文档承接上一阶段总结&#xff0c;聚焦于 2026年8月16日至8月18日 的第二轮深度部署过程&#xff0c;涵盖从源码编译失败到最终稳定运行、配置模板化、备份与迁移、测试机验证的全链路实践。所有内容基于真实对话记录整理&#xff0c;保留…

作者头像 李华
网站建设 2026/8/24 14:02:54

k8s的部署

我每条代码的都有知识拓展&#xff0c;能让大家在实验中理解底层逻辑一.实验主机配置harbor主机&#xff1a;eth0-172.25.254.254master主机&#xff1a;eth0-172.25.254.100node1主机&#xff1a;eth0-172.25.254.10node2主机&#xff1a;eth0-172.25.254.20 …

作者头像 李华