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