作为totW#130最初发表于2017年2月17日
由Titus Winters (titus@google.com)创作
命名的准确性夺去了所见的唯一性——皮埃尔·勃纳尔。
Google C++ Style Guide 的最早提交包含许多人仍在使用的命名空间命名指南。 粗略地说,这可以概括为“命名空间源自包路径”。 紧跟 Java 的包命名要求,这很有意义:我们希望能够唯一标识 C++ 中的符号,并且我们希望命名空间选择具有唯一性和一致性。
实际上,我们并没有。 我们只是将近十年都没有意识到。
名称查找
让我们从名称查找在 C++ 中的工作原理以及它与 Java 的不同之处开始。
namespace foo {
namespace bar {
void f() {
Baz b;
}
}
}
在 C++ 中,查找非限定名称 (Baz) 将在扩展范围内搜索同名符号:首先在 f()(函数)中,然后在 bar 中,然后在 foo 中,然后在全局命名空间中。
在 Java 中,没有非限定符号之类的东西:符号是限定名称:
public void f() {
com.google.foo.bar.Baz b = new com.google.foo.bar.Baz();
}
在任何情况下,Baz 都不会在明确提供的包之外进行查找:通配符不会下降到子包中,搜索也不会扩展到父包中。 事实证明,在 Java 和 C++ 中如何处理父包/命名空间的这种差异是为什么结构命名空间命名(使命名空间结构与包层次结构匹配)在 C++ 中是一个错误的基础。
问题
从包中构建命名空间的基本问题是我们很少依赖 C++ 中的完全限定查找,通常编写 std::unique_ptr 而不是 ::std::unique_ptr。再加上封闭命名空间中的查找,这意味着对于深度嵌套包(如::division::section::team::subteam::project)中的代码,任何不完全限定的符号(std::unique_ptr) 实际上可以引用任何:
- ::std::unique_ptr ::division::std::unique_ptr
- ::division::section::std::unique_ptr
- ::division::section::team::std::unique_ptr
- ::division::section::team::subteam::std::unique_ptr
- ::division::section::team::subteam::project::std::unique_ptr
更糟糕的是:不合格搜索从该列表的底部开始,一旦有名称空间匹配就停止。 这意味着如果您的任何传递包含添加一个以前未使用的名称空间,该名称空间与您在非限定名称空间中使用的任何符号的前导名称空间匹配,那么您的构建可能会被破坏。 严格来说,这甚至不一定是构建中断:如果有人添加了具有匹配名称和语法兼容 API 的内容,则该 API 的实现可能完全不兼容,并在运行时造成广泛的破坏。 显然,这对 std 来说还不错——没有人应该添加嵌套的命名空间 std——但是更常见的命名空间呢? 测试之类的呢?
名称不会被选择为唯一的。由于团队通常创建本地实用程序包来处理与他们所依赖的基础设施相关的常见任务,因此我们最终使用本地实用程序和管道包 - 以及子命名空间。 这是不必要和意外碰撞的秘诀。
相比之下,Java中的问题要少得多:如果您从Java中的两个包导入通配符,并且其中一个添加了一个与另一个包同名的新符号,那么您的构建可能会中断。 这可以通过禁止通配符导入轻松而完全地解决,就像在许多 Java 样式中所做的那样。
两种一致的选择,三种方法
这里有两个功能可以阻止这种远程构建的中断。
- 如果没有叶子命名空间 (search::foo::bar) 匹配任何顶级命名空间 (::bar) 或该叶子的任何父节点 (search::bar) 的子命名空间,则不会发生名称冲突。
- 如果没有不合格的查找,就不会有问题。
这里有(至少)三种方法完成这个:
- 始终完全限定当前命名空间之外的所有内容。 这非常冗长而且有点奇怪:C++(包括标准库)中的任何内容都没有在每个符号上使用前导 :: 编写。
- 构建一些工具来识别新命名空间的引入,并确保它不会与同一层次结构中的任何其他命名空间重叠。 也就是说,如果存在 ::bar 或 search::foo::bar,则不要添加 search::bar。
- 不要嵌套太深:每个项目的单个顶级命名空间得到相同的结果,没有冗长/复杂的名称,更少的事故风险,不会给新工程师带来惊讶,也不需要构建任何工具。
当前的样式指南建议使用最后一个选项,但如有必要,允许使用旧样式(命名空间匹配包名称)。 这主要是因为谷歌不想引起太多焦虑或触发任何人重新命名空间。 也就是说,如果我们让它在一个新的代码库中重新做一遍,我们会毫不含糊地说:每个项目都有一个公共接口的顶级命名空间。 通过通用数据库确保命名空间的唯一性。 因此,我们(仅)获得了像 absl 这样的顶级命名空间,并且在查找中不会有歧义(除非局部符号与全局命名空间中的符号发生冲突,但现代规则无论如何都不鼓励全局命名空间)。
因为在此更改之前存在大量代码,并且即使在此更改之后也有很多代码遵循旧模式,我们发现自己处于一种中间空间,一些命名空间通常需要完全限定(:: util),还有一些显然是独一无二的,永远不需要是 (std)。
但它让事情井井有条!
我经常听到人们说小/嵌套的命名空间“让事情井井有条”。 把东西放在他们的位置感觉是对的 - 为什么把像 StrCat() 和 make_unique() 这样的东西放在一起,而不是在 Abseil 中,它们彼此无关! absl::strings::utilities 命名空间不会有助于区分 absl::smart_ptrs 吗?
在其他语言中,这可能会很好 - 没有缺点的更好的组织。 但是,由于查找的工作方式(扩展到包含命名空间范围的连续层),您的细粒度命名空间会受到每个父命名空间中添加的每个符号(和子命名空间)的影响。 那就是:虽然你不完全“包含”来自父命名空间的名称,但名称/命名空间冲突几乎和你一样重要。 小型/深度嵌套的命名空间并不能保护您免受这种情况的影响,它们会加剧这种情况。
最佳实践
实际来讲,考虑到大多数代码库的实际情况,以下是我们能做的最好的事情:
- 有一个代码库的某种形式的数据库来识别唯一的命名空间。
- 引入新命名空间时,使用该数据库并将其作为顶层引入。
- 如果由于某种原因,上述是不可能的,那么永远不要引入与著名的顶级命名空间匹配的子命名空间。 没有用于 absl、testing、util 等的子命名空间。尝试为子命名空间提供唯一的名称,这些名称不太可能与未来的顶层冲突。
- 根据 TotW 119,声明命名空间别名和使用声明时,请使用完全限定名称,除非您指的是当前命名空间内的名称。
- 对于 util 或其他经常被滥用的命名空间中的代码,尽量避免完全限定,但在必要时限定。
TotW 119 中的建议对于 .cc 文件也有帮助:我们对完全限定的关注不是它不好,而是与世界其他地方的 C++ 代码相比它很奇怪。 在 using-declarations 中的有限使用达到了可接受的平衡。 然而,即使完全遵守这个建议也不能完全减轻不合格名称查找的危险,因为我们仍然有头文件,并且不想完全限定每个头文件中的每个符号。