本周小贴士#116: Using声明和命名空间别名

作为totW#119最初发表于2016年7月14日

由Thomas Köppe (tkoeppe@google.com)创作

此贴士提供一个简单、健壮的方法,用于在.cc文件中编写using声明和命名空间别名来避免不易觉察的陷阱。

在我们深入研究细节之前,这里有一个正在运行中的方法示例:

namespace example {
namespace makers {
namespace {

using ::otherlib::BazBuilder;
using ::mylib::BarFactory;
namespace abc = ::applied::bitfiddling::concepts;

// 私有的协助代码

}  // 命名空间

// 接口执行代码

}  // 命名空间标记
}  // 命名空间示例

请记住,此贴士中的所有内容仅适用于.cc文件,由于你永远不应该将便利的别名放在头文件中。这样的别名对于执行人(和实现的读者)来说是一种便利,但不是一种导出的设施。(作为导出API的一部分的名称当然可以在头文件中声明。)

概要

  • 永远不要在头文件的命名空间作用域内声明命名空间或using声明,只能在’.cc’文件中。
  • 在最里面的命名空间中声明命名空间别名或using声明,不论是有名的还是匿名的。(不要为了此目的添加匿名的命名空间)
  • 在声明命名空间别名或using声明时,除非你正在引用当前命名空间内的名字,否则请使用完整限定名(使用’::'引导)。
  • 对于名称的其他用法,合理的情况下可以避免使用完整的限定,参见TotW 130.
    (请记住,你始终可以在块作用域内使用本地命名空间别名或using声明,这在仅包含头文件的库中非常方便。)

背景

C++将名字放在命名空间中。这个关键的设施允许代码库以保有名称所有权本地化来扩展代码库,避免与在其他作用域中的代码发生冲突。然而,命名空间强加一定的装饰负担,由于限定命名(foo::Bar)一般很长且容易变得混乱。我们经常发现使用非限定名(Bar)更方便。另外,我们可能希望针对长且经常使用的命名空间引入命名空间别名:namespace eu = example::v1::util;在本贴士中,我们统一将using声明和命名空间别名称为别名。

问题

命名空间的目的是帮助代码作者在名称查找或链接时避免命名冲突。别名可能破坏命名空间提供的保护。这个问题有两个不同的方面:别名的作用域,相对限定符的使用。

别名的作用域

你放置别名的作用域会对代码的可维护性产生微妙的影响。考虑以下两种变体:

using ::foo::Quz;

namespace example {
namespace util {

using ::foo::Bar;

看起来两个using声明在使名称Bar和Quz在我们的命名空间::example::util中进行非限定查找中都是有效的。对于Bar,一切都在按预期进行,前提是是命名空间::example::util中没有其他的Bar声明。但这是你的命名空间,因此你可以控制它。

另一方面,如果稍后引入的头文件声明全局名称Quz,接着第一个using声明将变得不正确,因为它尝试再次声明名字Quz。如果另一个头文件声明::example::Quz或::example::util::Quz,则非限定查找将找到该名称而不是你的别名。

如果你不将名称添加到你不拥有的命名空间中(包括全局命名空间),那么可以避免这种脆弱性。通过将别名放置在你自己的命名空间中,非限定查找首先找到你的别名,并且永远不会继续搜索包含命名空间的内容。

更一般来讲,声明越接近使用点,破坏代码的范围集就越小。在我们示例中最糟糕的是Quz,它可以被任何人破坏;Bar只能被其他在::example::util中的代码破坏,并且在未具名的命名空间中声明和使用的名称不会被任何其他作用域破坏。有关示例,请查阅未具名命名空间。

相对限定符

using foo::Bar形式的using声明似乎是无害,但事实上是容易歧义的。问题是依赖在命名空间中已经存在的名字是安全的,但是依赖不存在的名字则是不安全的。考虑这个代码:

namespace example {
namespace util {

using foo::Bar;

使用名称::foo::Bar可能是作用的意图。但是,这可能中断,因为代码依赖于::foo::Bar的存在以及命名空间::example::foo和::example::util::foo的不存在。通过完整地使用::foo::Bar来限定可以避免这种脆弱性。

相对名称唯一明确且不可能被外部声明破坏的情况是它引用一个已经在当前命名空间内的名称:

namespace example {
namespace util {
namespace internal {

struct Params { /* ... */ };

}  // 命名空间internal

using internal::Params;  // 可行,与::example::util::internal::Params相同

这遵循我们在上一节中讨论中的相同逻辑。

如果一个名称位于同级命名空间中,例如::example;:tools::Ting,该怎么办?你可以说tools::Thing或::example::tools::Thing。完整限定的名称总是正确的,但是也可能使用相对名称更合适。取决于你自己的判断。

不要在你的项目中使用与流行的顶层命名空间(如util)相同的命名空间是一种代价小的方法;风格指南明确推荐这种实践。

示例

下面的代码示范了两种失败模式。
helper.h:

namespace bar {
namespace foo {

// ...

}  // 命名空间foo
}  // 命名空间bar

some_feature.h:

extern int f;

Your code:

#include "helper.h"
#include "some_feature.h"

namespace foo {
void f();
}  // 命名空间foo

// 失败模式#1:在错误的作用域中的别名
using foo::f;  // 错误:重复声明(因为"f"在some_feature.h中)

namespace bar {

// 失败模式#2:别名糟糕地限定
using foo::f;  // 错误:在命名空间中没有"f"(因为这个命名空间声明在helper.h中) 

// 推荐的方式,面对未相对的声明是健壮的: declarations:
using ::foo::f;  // OK

void UseCase() { f(); }

}  // 命名空间bar

未具名的命名空间

放置在未具名的命名空间中的using声明可以从封装命名空间来访问,反之亦然。如果文件顶部有一个未具名命名空间,最好将所有别名放在这里。从这个未具名的命名空间中,你可以获得额外的健壮,以免与在封闭的命名空间中的声明的内容冲突。

namespace example {
namespace util {

namespace {

// 将所有using声明放在此处。不要分散它们到文件中。
using ::foo::Bar;
using ::foo::Quz;

// 在此处,Bar和Quz与你的别名不分离。

}  // 命名空间

// 此处也能使用Bar和Quz。(但是你自己不要声明任何实体为Bar和Quz)

非别名名称

到目前为止,我们一直在讨论远处名称的本地别名。但是如果我们直接使用名称而不是创建别名,会发生什么呢?我们应该说util::Status还是::util::Status呢?

这里没有明确的答案。不同于之前我们讨论过的别名声明,它出现在远离实际代码的文件顶部,名称的直接使用影响本地代码的可读性。虽然相对限定名称未来可能出问题,但是使用绝对限定的成本很高。引导的::引起的视觉混乱可能分散注意力,并且增加的健壮性是不值得的。在这种情况下,请自行决定优先使用哪种风格。参见TotW 130.

致谢

所有的功劳全归于Roman Perepelitsa (romanp@google.com),他在邮件列表讨论中建议了这种风格,并且贡献了许多更正和关键点。然而,所有的错误都是我的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值