天天看点

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

(原文链接:https://abseil.io/tips/153 译者:[email protected])

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

  • 最初发布于:2018-07-17
  • 作者:Roman Perepelitsa 和 Ashley Hedberg
  • 更新于:2020-04-06
  • 短链接: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指示

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

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::Sequence

    seq

    将会指代那个实体而不是

    ::testing::Sequence

  • 如果任何人定义了

    ::Sequence

    seq

    的定义将会编译失败,因为对

    Sequence

    这个名字的引用将会产生歧义。

    Sequence

    可能意味着

    ::testing::Sequence

    ::Sequence

    ,编译器不知道你想要哪个。
  • 如果任何人定义了

    ::testing::WallTimer

    timer

    的定义将会编译失败。

因此,单单一个函数里的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

……

_9

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

继续阅读