Google C++每周贴士 #153: 别用using指示

(原文链接:https://abseil.io/tips/153 译者:clangpp@gmail.com)

每周贴士 #153: 别用using指示

在我看来using指示就是定时炸弹,不论是对它处理的部分还是类型系统。Ashley Hedberg 带着对Warren Buffett的歉意

长话短说(tl;dr)

using指示(using namespace foo)足够危险,以至于被Google风格指南禁用了。

如果你想让名字短一点,可以改用命名空间别名(namespace baz = ::foo::bar::baz;)或using声明(using ::foo::SomeName),它俩都在一定的上下文中被风格指南所允许(例如,在*.cc文件中)。

函数作用域中的using指示

你觉得这段代码干了什么?

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指示把名字注入到了它被声明的地方。在上面的例子中,也就是函数作用域。事实上,这些名字被注入到了目标命名空间(::testing)和用例命名空间(::totw::example::anonymous)的最近的公共祖先里。在我们的例子中,这货是全局命名空间

因此,这段代码大概相当于:

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指示,就算只是在函数作用域中,也在全局和其他命名空间中创造了足够多的命名冲突的机会。

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

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,大概相当于:

namespace proto = ::testing::proto;

namespace totw {
namespace example {
namespace {

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

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

该测试会一直通过编译,直到间接地include了一个定义了命名空间::proto::totw::proto::totw::example::proto的头文件为止。到那个时候,proto::Partially变得有歧义了,该测试编译失败。这就联系到了风格指南里的命名空间命名规则:避免嵌套命名空间,而且不要用常用名字为嵌套命名空间起名字。(更多相关信息,请参考Tip #130命名空间名字

有同学可能觉得,对一个封闭的、只有很少符号且保证不会增加符号的命名空间,使用using指示就是安全的。(只有符号_1……_9std::placeholders就是这种命名空间的一个例子。)然而,就算这样也不安全:它阻止了其他任何命名空间引入同名的符号。在这个意义上,using指示打破了命名空间提供的模块性。

未限定的using指示

我们已经见识过单个using指示可能走到沟里。如果在同一个代码库里有很多——未限定的——using指示,那将是一份怎样的精彩呢?

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的子命名空间被引入,代码就会编译失败。(实际上我们(译者注:Google)已经见过那个命名空间了,所以这段代码和那个命名空间相遇只是时间问题。这也是避免深层的嵌套命名空间的另一个原因。)命名空间限定的缺失在这里很重要:如果using指示被完全限定 而且 没有对两个命名空间内共有的名字的非限定的命名查找,那么这个代码片段还是有可能编译通过的。
  • ::totw::example::totw::testing::rpc或全局命名空间里引入一个新的符号,有可能跟 任一命名空间 中的已有符号冲突。这是个天坑。

悄悄话:你觉得RPC在哪个命名空间内?rpc是个合理的猜测,但它实际上在全局命名空间里。除了维护问题以外,这里的using指示还让代码很难读。

那这个特性为什么会存在?

在通用的库中有using指示的合理应用,但它们既晦涩又少见,以至于不值得在这里或风格指南中花篇幅讨论它们。

临别赠言

using指示是定时炸弹:今天还能编译的代码,在下一个语言版本或增加符号之后,分分钟编译失败给你看。对于短命且依赖关系从不改变的外层代码,这也许是一个可接受的风险。但是请注意:如果你后来决定想要让你的临时项目在长时间内继续工作,这些定时炸弹也许会爆炸。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值