每周技巧 #119:using 声明和命名空间别名

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #119: Using-declarations and namespace aliases

原文最初作为 totw/119 发布于 2016 年 7 月 14 日。

作者:Thomas Köppe

本技巧给出一个简单、稳健的做法,用于在 .cc 文件中编写 using 声明和命名空间别名,并避免微妙陷阱。

进入细节之前,先看一个应用该做法的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
namespace example {
namespace makers {
namespace {

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

// 私有辅助代码在这里。

}  // namespace

// 接口实现代码在这里。

}  // namespace makers
}  // namespace example

请记住,本技巧中的所有内容只适用于 .cc 文件,因为你绝不应该把便利别名放进头文件。这类别名是给实现者(以及实现代码读者)的便利,不是导出的设施。(当然,确实属于导出 API 的名字可以声明在头文件中。)

总结

  • 绝不要在头文件的命名空间作用域声明命名空间别名或便利 using 声明,只在 .cc 文件中这样做。
  • 在最内层命名空间中声明命名空间别名和 using 声明,无论该命名空间是具名还是匿名。(不要只为了这个目的添加匿名命名空间。)
  • 声明命名空间别名和 using 声明时,除非你引用的是当前命名空间内的名字,否则使用完全限定名(带前导 ::)。
  • 对名称的其他使用,在合理时避免完全限定,见 TotW 130

(请记住,你总是可以在块作用域中拥有局部命名空间别名或 using 声明,这在 header-only 库中可能很方便。)

背景

C++ 用命名空间组织名称。这个关键设施让代码库能够扩展:通过把名称所有权保持在局部,避免其他作用域中的名称冲突。不过,命名空间会带来一定外观负担,因为限定名(foo::Bar)通常很长,很快会变得杂乱。我们常常觉得使用非限定名Bar)更方便。此外,我们可能想为一个很长但频繁使用的命名空间引入命名空间别名:namespace eu = example::v1::util; 本技巧中,我们把 using 声明和命名空间别名统称为别名

问题

命名空间的目的是帮助代码作者避免名称冲突,包括名称查找时的冲突和链接时的冲突。别名可能削弱命名空间提供的保护。这个问题有两个独立方面:别名的作用域,以及相对限定名的使用。

别名的作用域

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

1
2
3
4
5
6
using ::foo::Quz;

namespace example {
namespace util {

using ::foo::Bar;

看起来两个 using 声明都能让名称 BarQuz 在我们的工作命名空间 ::example::util 内通过非限定查找可用。对 Bar 来说,只要命名空间 ::example::util 内没有其他 Bar 声明,一切都按预期工作。但这是你的命名空间,所以你有能力控制这一点。

另一方面,如果之后包含了一个声明全局名称 Quz 的头文件,那么第一个 using 声明会变成病式,因为它试图重新声明名称 Quz。如果另一个头文件声明了 ::example::Quz::example::util::Quz,那么非限定查找会找到那个名称,而不是你的别名。

如果你不向自己不拥有的命名空间(包括全局命名空间)添加名称,就可以避免这种脆弱性。把别名放在你自己的命名空间里,非限定查找会首先找到你的别名,并且不会继续搜索包含命名空间。

更一般地说,声明离使用点越近,能破坏你代码的作用域集合就越小。在我们的例子中,最糟糕的是 Quz,任何人都可以破坏它;Bar 只能被 ::example::util 中的其他代码破坏;而在匿名命名空间中声明并使用的名字,不会被任何其他作用域破坏。示例见匿名命名空间

相对限定名

形如 using foo::Barusing 声明看起来无害,但实际上有歧义。问题在于,依赖某个命名空间中名称的存在是安全的,但依赖名称的不存在是不安全的。考虑这段代码:

1
2
3
4
namespace example {
namespace util {

using foo::Bar;

作者的意图也许是使用名称 ::foo::Bar。然而这可能会坏掉,因为代码依赖 ::foo::Bar 的存在,同时也依赖命名空间 ::example::foo::example::util::foo 不存在。通过完全限定被使用的名称可以避免这种脆弱性:using ::foo::Bar

只有当相对名称引用的是已经位于当前命名空间内部的名称时,它才是无歧义且不会被外部声明破坏的:

1
2
3
4
5
6
7
8
9
namespace example {
namespace util {
namespace internal {

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

}  // namespace internal

using internal::Params;  // OK,等同于 ::example::util::internal::Params

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

如果一个名称位于兄弟命名空间中,例如 ::example::tools::Thing,该怎么办?你可以写 tools::Thing,也可以写 ::example::tools::Thing。完全限定名总是正确的,但使用相对名称也可能合适。请自行判断。

避免许多这类问题的廉价方式,是不要在你的项目中使用和流行顶层命名空间(例如 util)相同的命名空间;风格指南明确推荐这种做法

演示

下面的代码展示了两种失败模式。

helper.h:

1
2
3
4
5
6
7
namespace bar {
namespace foo {

// ...

}  // namespace foo
}  // namespace bar

some_feature.h:

1
extern int f;

你的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include "helper.h"
#include "some_feature.h"

namespace foo {
void f();
}  // namespace foo

// 失败模式 #1:别名位于糟糕作用域。
using foo::f;  // 错误:重新声明(因为 some_feature.h 中声明了 "f")

namespace bar {

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

// 推荐方式:面对无关声明时仍然稳健。
using ::foo::f;  // OK

void UseCase() { f(); }

}  // namespace bar

匿名命名空间

放在匿名命名空间中的 using 声明可以从外围命名空间访问,反之亦然。如果你在文件顶部已经有匿名命名空间,优先把所有别名都放在那里。从该匿名命名空间内部看,你会获得一点额外稳健性,避免与外围命名空间中声明的东西冲突。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace example {
namespace util {

namespace {

// 把所有 using 声明放在这里。不要把它们散布在文件各处。
using ::foo::Bar;
using ::foo::Quz;

// 在这里,Bar 和 Quz 不可剥夺地指向你的别名。

}  // namespace

// 这里也可以使用 Bar 和 Quz。(但现在不要自己声明任何叫 Bar 或 Quz 的实体。)

非别名名称

到目前为止,我们一直在谈远处名称的局部别名。但如果我们想直接使用名称,而完全不创建别名呢?应该写 util::Status 还是 ::util::Status

没有明显答案。和前面讨论的别名声明不同,别名声明出现在文件顶部,离实际代码很远;而直接使用名称会影响代码的局部可读性。虽然相对限定名未来确实可能坏掉,但使用绝对限定也有显著成本。前导 :: 造成的视觉杂乱可能会分散注意力,不值得用来换取额外稳健性。在这种情况下,请自行判断偏好的风格。见 TotW 130

致谢

所有功劳都归 Roman Perepelitsa (romanp@google.com),他最初在邮件列表讨论中建议了这种风格,并贡献了许多修正和妙语。不过,所有错误都是我的。

每周技巧 #117:拷贝消除和按值传递

上一节

每周技巧 #120:返回值不可触碰

下一节