每周技巧 #153:不要使用 using 指示

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #153: Don’t Use using-directives

原文最初作为 TotW #153 发布于 2018 年 7 月 17 日。

作者:Roman PerepelitsaAshley 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 指示

你认为下面这段代码会做什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace totw {
namespace example {
namespace {

TEST(MyTest, UsesUsingDirectives) {
  using namespace ::testing;
  Sequence seq;  // ::testing::Sequence
  WallTimer timer;  // ::WallTimer
  ...
}

}  // namespace
}  // namespace example
}  // namespace totw

绝大多数 C++ 用户认为 using 指示会把名称注入它声明所在的作用域。在上面的例子中,那就是函数作用域。现实中,在 using 指示处于作用域期间,名称会被注入目标命名空间(::testing)和使用命名空间(::totw::example::anonymous)最近的共同祖先。在我们的例子中,那是全局命名空间

因此,这段代码大致等价于下面这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using ::testing::Expectation;
using ::testing::Sequence;
using ::testing::UnorderedElementsAre;
...
// 许许多多符号被注入全局命名空间

namespace totw {
namespace example {
namespace {

TEST(MyTest, UsesUsingDirectives) {
  Sequence seq; // ::testing::Sequence
  WallTimer timer; // ::WallTimer
  ...
}

} // namespace
} // namespace example
} // namespace totw

这个变换并不完全准确,因为这些名称实际上不会在 using 指示作用域之外继续可见。不过,即使只是临时注入全局作用域,也会带来一些不幸后果。

看看哪些变更会破坏这段代码:

  • 如果有人定义 ::totw::Sequence::totw::example::Sequenceseq 现在会引用该实体,而不是 ::testing::Sequence
  • 如果有人定义 ::Sequenceseq 的定义会编译失败,因为对名称 Sequence 的引用会变得有歧义。Sequence 可以表示 ::testing::Sequence,也可以表示 ::Sequence,编译器不知道你想要哪个。
  • 如果有人定义 ::testing::WallTimertimer 的定义会编译失败。

因此,函数作用域中的单个 using 指示,就给 ::testing::totw::totw::example 和全局命名空间中的符号施加了命名限制。允许这个 using 指示,即使只是在函数作用域中,也会在全局和其他命名空间中创造大量名称冲突机会。

如果这个例子看起来还不够脆弱,考虑下面这个:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace totw {
namespace example {
namespace {

TEST(MyTest, UsesUsingDirectives) {
  using namespace ::testing;
  EXPECT_THAT(..., proto::Partially(...)); // ::testing::proto::Partially
  ...
}

} // namespace
} // namespace example
} // namespace totw

这个 using 指示在全局命名空间中引入了一个命名空间别名 proto,大致等价于:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace proto = ::testing::proto;

namespace totw {
namespace example {
namespace {

TEST(MyTest, UsesUsingDirectives) {
  EXPECT_THAT(..., proto::Partially(...)); // ::testing::proto::Partially
  ...
}

} // namespace
} // namespace example
} // namespace totw

测试会继续编译,直到某个定义命名空间 ::proto::totw::proto::totw::example::proto 的头文件被传递包含。那时 proto::Partially 会变得有歧义,测试停止编译。这与风格指南中的命名空间命名规则相关:避免嵌套命名空间,不要给嵌套命名空间使用常见名称。(关于这个主题,更多内容见 技巧 #130https://google.github.io/styleguide/cppguide.html#Namespace_Names。)

有人可能认为,对于一个符号很少、并保证永远不会添加更多符号的封闭命名空间,使用 using 指示是安全的。(std::placeholders 包含 _1_9,就是这种命名空间的例子。)然而,即使那样也不安全:它会阻止任何其他命名空间引入同名符号。从这个意义上说,using 指示击败了命名空间提供的模块化。

非限定 using 指示

我们已经看到一个 using 指示会如何出错。如果同一个代码库中有许多非限定 using 指示,会发生什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace totw {
namespace example {
namespace {

using namespace rpc;
using namespace testing;

TEST(MyTest, UsesUsingDirectives) {
  Sequence seq;  // ::testing::Sequence
  WallTimer timer;  // ::WallTimer
  RPC rpc;  // ...is this ::rpc::RPC or ::RPC?
  ...
}

}  // namespace
}  // namespace example
}  // namespace totw

这里可能出什么问题?事实证明,很多:

  • 函数级例子中的所有问题仍然存在,而且变成两份:一次针对命名空间 ::testing,一次针对命名空间 ::rpc
  • 如果命名空间 ::rpc 和命名空间 ::testing 声明了同名符号,当这段代码对这些名称之一进行非限定查找时,就无法编译。这很重要,因为它展示了一个可怕的扩展性问题:由于每个命名空间的全部内容(一般来说)都会被注入全局命名空间,每个新的 using 指示都可能以二次方增加名称冲突和构建失败风险。
  • 如果将来引入了 ::rpc::testing 这样的子命名空间,这段代码会停止编译。(我们实际上见过这个命名空间,所以这个片段和那个命名空间被一起构建可能只是时间问题。这也是避免深度嵌套命名空间的另一个理由。)这里缺少命名空间限定很重要:如果 using 指示是完全限定的,且没有对两个命名空间共有名称进行非限定查找,这个片段也许可以编译。
  • ::totw::example::totw::testing::rpc 或全局命名空间中新引入的符号,可能与这些命名空间中任意一个已有符号冲突。这是一个很大的可能性矩阵。

简短插一句:你觉得 RPC 位于哪个命名空间?rpc 本来是个完全合理的猜测,但它实际上位于全局命名空间。撇开可维护性问题不谈,这里的 using 指示也让代码难以阅读。

那为什么会有这个特性?

在泛型库内部,using 指示有合法用途,但它们太晦涩、太罕见,不值得在这里或风格指南中提及。

结语

using 指示是定时炸弹:今天能编译的代码,很容易在下一个语言版本或符号添加后停止编译。对于生命周期很短且依赖永不变化的外部代码,这也许是可接受风险。但请注意:如果你后来决定让这个短命项目随时间推移继续工作,这些定时炸弹可能会爆炸。

每周技巧 #152:`AbslHashValue` 与你

上一节

每周技巧 #158:Abseil 关联容器和 `contains()`

下一节