每周小贴士#153:不要使用using指令

using指令在C++中可能引发命名空间污染和意外的符号冲突,尤其是在全局和函数作用域中。Google风格指南建议避免使用using指令,推荐使用命名空间别名或局部using声明来减少影响范围,以维护代码的模块性和可维护性。
摘要由CSDN通过智能技术生成

作为TotW#153最初发表于2018年7月17日

由Roman Perepelitsa和Ashley Hedberg创作

我认为使用指令(using-directives)就像是定时炸弹,对于使用它们的人和语言系统都是如此。——Ashley Hedberg(向沃伦·巴菲特致歉)

简而言之

Google风格指南禁止使用using指令(如using namespace foo),因为它们太危险了。不要在需要升级的代码中使用它们。

如果你想缩短名称,可以使用命名空间别名(namespace baz = ::foo::bar::baz;)或使用声明(using ::foo::SomeName),在某些情况下(如*.cc文件)这两种方式都被允许。

函数作用域中的using指令

你认为这段代码是做什么的?

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,那么计时器的定义将无法编译。
    因此,一个函数作用域中的单个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

这个测试将一直编译,直到包含传递定义命名空间::proto、::totw::proto或::totw::example::proto的头文件。此时,proto::Partially变得模棱两可,测试停止编译。这与风格指南中关于命名空间命名的规则有关:避免嵌套命名空间,并且不要为嵌套命名空间使用常见名称。 (有关此主题的更多信息,请参见Tip#130和https://google.github.io/styleguide/cppguide.html#Namespace_Names。)

有人可能认为,对于一个具有少量符号并保证不会再添加任何符号的封闭命名空间,使用using指令是安全的。(std::placeholders包含符号_1…_9,就是这样一个命名空间的例子。)然而,即使这也不安全:它排除了任何其他命名空间引入具有相同名称的符号。从这个意义上讲,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,则此代码将停止编译。(我们实际上已经看到了该命名空间,因此这段代码和该命名空间一起构建的可能性很大。这是避免深度嵌套命名空间的另一个原因)。在这里缺少命名空间限定很重要:如果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、付费专栏及课程。

余额充值