(原文链接:https://abseil.io/tips/186 译者:clangpp@gmail.com)
每周贴士 #186: 最好把函数放在匿名命名空间里
- 最初发布于:2020-11-05
- 作者:James Dennett 和 Jason Rennie
- 更新于:2020-11-05
- 短链接:abseil.io/tips/186
“Everything should be made as simple as possible, but no simpler.” ~ Roger
Sessions’s interpretation of Einstein
事情应该力求简单,不过不能过于简单。——罗杰·塞欣斯对爱因斯坦的解读
添加新的函数时,默认把它写成调用者.cc
文件里的本地非成员函数。虽然有合理的原因可以做其他的选择,但仍请考虑把它写在未命名的命名空间里(unnamed namespace)(也被称作“匿名命名空间(anonymous namespace)”)。
好处
匿名命名空间中的非成员函数有两个好处,既让函数内附于一个.cc
文件(挪出头文件),又让函数成为非成员函数(挪出类)。
相比于头文件内声明的函数,其好处包括:
- 读者更容易查找函数定义(因为它跟调用端处在同一个文件,且在第一次调用之前)。
- 把文档、声明、定义放在同一个地方(相比于声明在头文件,定义在源文件这种两个地方的情况)。
- 与其他源文件隔离开,使其更容易重构。
- 不再需要担心有意义的
const
,因为没有额外的声明了。 - 和其他的库实现的辅助代码(implementation helpers)在同一个地方,比如方便的别名(convenience aliases)和本地类型。参照Tip of the Week #119: Using-declarations and Namespace Aliases。
- 提供附带收益,比如允许一个类型也被挪到匿名命名空间(如果它只被一个源文件引用的话)。
相比于私有成员函数,其好处包括:
- 输入输出更清晰,因为它们(更可能)以参数和返回值来指定。需要注意的是,一个成员函数可以读取任意数据成员,一个非常(non-const)成员函数可能会修改任意非常(non-const)数据成员。与此相反,一个非成员函数只能依照其接口进行读取和修改(全局变量除外)。
- 类API可以更简单和更短,因此更容易阅读——非必要的私有成员函数可能让你更难找到继承相关的私有声明或类之后的声明。
什么时候别这么干(Reasons to Look Elsewhere)
有时候本地非成员函数并不合理。举例来说:
- 当函数在多个源文件都有用的时候。请在头文件声明之,以便复用。
- 当函数与一个对象或类之间有复杂的交互的时候。例如,如果一个函数读取了多个数据成员,而且需要修改对象状态,且不容易写成返回值赋值的形式,那么它很可能应该被写成成员函数。特别来说,包括了互斥量(mutex)的逻辑通常会属于一个成员函数。
- 当函数属于类的API的时候。
备选项:静态(static
)非成员函数
在隔离不同编译单元(translation units)代码的意义上,把非成员函数标记为static
,和把它放进匿名命名空间基本是等效的。虽然匿名命名空间以一致的方式覆盖类型、函数和对象,但是有些人喜欢看到static
被显式地写在函数声明处,这样就可以在不检查外层的匿名命名空间的情况下,也能知道它从属于本地的编译单元。虽然本贴士推荐使用匿名命名空间,但是使用static
也可以是一个合理的选项。
其他参考资料
部分风格指南把我们引向这个方向,但它们没有本贴士走得这么远。例如:
- 匿名命名空间章节鼓励把函数定义放在匿名命名空间内(或声明为
static
),但没有讨论私有成员函数。 - 输入输出章节鼓励使用返回值,但没有提及数据成员的修改(通过
this
);this
基本上是一个囫囵的输入输出参数。 - 本地变量章节鼓励最小化变量作用域,但没有将其拓展到类和对象。
总结
文件内本地函数能简化依赖关系和提高本地化(locality)。非成员函数能提升封装性,简化类定义,以及让依赖关系更明显。写函数的时候,考虑将其设计为文件本地非成员函数,比如通过将其放进.cc
文件的匿名命名空间内。