每周技巧 #153:不要使用 using 指示
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #153: Don’t Use using-directives。
原文最初作为 TotW #153 发布于 2018 年 7 月 17 日。
作者:Roman Perepelitsa 和 Ashley Hedberg
更新于 2020 年 4 月 6 日。
快捷链接:abseil.io/tips/153
我把 using 指示视为定时炸弹,对处理它们的各方和语言系统都是如此。 —— Ashley Hedberg,向 Warren Buffett 致歉
TL;DR
using 指示(using namespace foo)非常危险,因此被 Google 风格指南禁止。不要在任何以后需要升级的代码中使用它们。
如果想缩短名称,可以改用命名空间别名(namespace baz = ::foo::bar::baz;)或 using 声明(using ::foo::SomeName);风格指南在某些上下文中允许这两者(例如在 *.cc 文件中)。
函数作用域中的 using 指示
你认为下面这段代码会做什么?
|
|
绝大多数 C++ 用户认为 using 指示会把名称注入它声明所在的作用域。在上面的例子中,那就是函数作用域。现实中,在 using 指示处于作用域期间,名称会被注入目标命名空间(::testing)和使用命名空间(::totw::example::anonymous)最近的共同祖先。在我们的例子中,那是全局命名空间!
因此,这段代码大致等价于下面这样:
|
|
这个变换并不完全准确,因为这些名称实际上不会在 using 指示作用域之外继续可见。不过,即使只是临时注入全局作用域,也会带来一些不幸后果。
看看哪些变更会破坏这段代码:
- 如果有人定义
::totw::Sequence或::totw::example::Sequence,seq现在会引用该实体,而不是::testing::Sequence。 - 如果有人定义
::Sequence,seq的定义会编译失败,因为对名称Sequence的引用会变得有歧义。Sequence可以表示::testing::Sequence,也可以表示::Sequence,编译器不知道你想要哪个。 - 如果有人定义
::testing::WallTimer,timer的定义会编译失败。
因此,函数作用域中的单个 using 指示,就给 ::testing、::totw、::totw::example 和全局命名空间中的符号施加了命名限制。允许这个 using 指示,即使只是在函数作用域中,也会在全局和其他命名空间中创造大量名称冲突机会。
如果这个例子看起来还不够脆弱,考虑下面这个:
|
|
这个 using 指示在全局命名空间中引入了一个命名空间别名 proto,大致等价于:
|
|
测试会继续编译,直到某个定义命名空间 ::proto、::totw::proto 或 ::totw::example::proto 的头文件被传递包含。那时 proto::Partially 会变得有歧义,测试停止编译。这与风格指南中的命名空间命名规则相关:避免嵌套命名空间,不要给嵌套命名空间使用常见名称。(关于这个主题,更多内容见 技巧 #130 和 https://google.github.io/styleguide/cppguide.html#Namespace_Names。)
有人可能认为,对于一个符号很少、并保证永远不会添加更多符号的封闭命名空间,使用 using 指示是安全的。(std::placeholders 包含 _1 到 _9,就是这种命名空间的例子。)然而,即使那样也不安全:它会阻止任何其他命名空间引入同名符号。从这个意义上说,using 指示击败了命名空间提供的模块化。
非限定 using 指示
我们已经看到一个 using 指示会如何出错。如果同一个代码库中有许多非限定 using 指示,会发生什么?
|
|
这里可能出什么问题?事实证明,很多:
- 函数级例子中的所有问题仍然存在,而且变成两份:一次针对命名空间
::testing,一次针对命名空间::rpc。 - 如果命名空间
::rpc和命名空间::testing声明了同名符号,当这段代码对这些名称之一进行非限定查找时,就无法编译。这很重要,因为它展示了一个可怕的扩展性问题:由于每个命名空间的全部内容(一般来说)都会被注入全局命名空间,每个新的 using 指示都可能以二次方增加名称冲突和构建失败风险。 - 如果将来引入了
::rpc::testing这样的子命名空间,这段代码会停止编译。(我们实际上见过这个命名空间,所以这个片段和那个命名空间被一起构建可能只是时间问题。这也是避免深度嵌套命名空间的另一个理由。)这里缺少命名空间限定很重要:如果 using 指示是完全限定的,且没有对两个命名空间共有名称进行非限定查找,这个片段也许可以编译。 ::totw::example、::totw、::testing、::rpc或全局命名空间中新引入的符号,可能与这些命名空间中任意一个已有符号冲突。这是一个很大的可能性矩阵。
简短插一句:你觉得 RPC 位于哪个命名空间?rpc 本来是个完全合理的猜测,但它实际上位于全局命名空间。撇开可维护性问题不谈,这里的 using 指示也让代码难以阅读。
那为什么会有这个特性?
在泛型库内部,using 指示有合法用途,但它们太晦涩、太罕见,不值得在这里或风格指南中提及。
结语
using 指示是定时炸弹:今天能编译的代码,很容易在下一个语言版本或符号添加后停止编译。对于生命周期很短且依赖永不变化的外部代码,这也许是可接受风险。但请注意:如果你后来决定让这个短命项目随时间推移继续工作,这些定时炸弹可能会爆炸。