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

Posted -飞鹤-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每周小贴士#153:不要使用using指令相关的知识,希望对你有一定的参考价值。

作为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指令是定时炸弹:今天编译的代码很容易在下一个语言版本或符号添加时停止编译。对于短暂存在且其依赖关系永远不会改变的外部代码,这可能是可以接受的风险。但要小心:如果您后来决定希望您的短暂项目继续随时间推移而工作,那些定时炸弹可能会爆炸。

以上是关于每周小贴士#153:不要使用using指令的主要内容,如果未能解决你的问题,请参考以下文章

每周小贴士#148:重载集

每周小贴士#65:就地安放

每周小贴士#142:多参数构造函数和explicit

每周小贴士#142:多参数构造函数和explicit

每周小贴士#64:原生字符串字面量

每周小贴士#158:Abseil关联窗口和contains()