10.4 命名空间

简介

大型程序往往会使用多个独立开发的库,这些库会定义大量的全局名字,如类、函数和模板等,不可避免会出现某些名字相互冲突的情况。命名空间namespace分割了全局命名空间,其中每个命名空间是一个作用域。

namespace foo {
    class Bar { /*...*/ };
}  // 命名空间结束后无需分号

命名空间定义

1. 每个命名空间都是一个作用域

同其他作用域类似,命名空间中的每个名字都必须表示该空间内的唯一实体。因为不同命名空间的作用域不同,所以在不同命名空间内可以有相同名字的成员。

2. 命名空间可以不连续

命名空间的定义可以不连续的特性使得我们可以将几个独立的接口和实现文件组成一个命名空间,定义多个类型不相关的命名空间也应该使用单独的文件分别表示每个类型。

3. 模板特例化

模板特例化必须定义在原始模板所属的命名空间中,和其他命名空间名字类似,只要我们在命名空间中声明了特例化,就能在命名空间外部定义它了:

// 我们必须将模板特例化声明成std的成员
namespace std {
    template <> struct hash<Foo>;
}

// 在std中添加了模板特例化的声明后,我们就可以在命名空间std的外部定义它了
template<> struct std::hash<Foo> {
    size_t operator()(const Foo& f) const {
        return hash<string>()(f.str) ^
            hash<double>()(f.d);
    }
};

4. 全局命名空间

全局作用域中定义的名字(即在所有类、函数以及命名空间之外定义的名字)也就是定义在全局命名空间global namespace中。全局作用域是隐式的,所以它并没有名字,下面的形式表示全局命名空间中一个成员:

::member_name

5. 嵌套的命名空间

namespace foo {
    namespace bar {
        class Cat { /*...*/ };
    }
}

// 调用方式
foo::bar::Cat

6. 内联命名空间

C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间inline namespace。内联命名空间可以被外层命名空间直接使用。定义内联命名空间的方式是在关键字namespace前添加关键字inline

// inline必须出现在命名空间第一次出现的地方
inline namespace FifthEd {
	// ...
}
// 后续再打开命名空间的时候可以写inline也可以不写
namespace FifthEd {  // 隐式内敛
    // ...
}

当应用程序的代码在一次发布和另一次发布之间发生改变时,常使用内联命名空间。例如我们把第五版FifthEd的所有代码放在一个内联命名空间中,而之前版本的代码都放在一个非内联命名空间中:

namespace FourthEd {
    // 第4版用到的其他代码
    class Cat { /*...*/ };
}

// 命名空间cplusplus_primer将同时使用这两个命名空间
namespace foo {
#include "FifthEd.h"
#include "FoutthEd.h"
} 

因为FifthEd是内联的,所以形如foo::的代码可以直接获得FifthEd的成员,如果我们想用到早期版本的代码,则必须像其他嵌套的命名空间一样加上完整的外层命名空间名字:

foo::FourthEd::Cat

7. 未命名的命名空间

关键字namespace后紧跟花括号括起来的一系列声明语句是未命名的命名空间unnamed namespace。未命名的命名空间中定义的变量具有静态生命周期:它们在第一次使用前被创建,直到程序结束时才销毁。

Tips:每个文件定义自己的未命名的命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。在这两个未命名的命名空间里面可以定义相同的名字,并且这些定义表示的是不同实体。如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。

和其他命名空间不同,未命名的命名空间仅在特定的文件内部有效,其作用范围不会横跨多个不同的文件。未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同,如果未命名的命名空间定义在文件的最外层作用域中,则该命名空间一定要与全局作用域中的名字有所区别:

// i的全局声明
int i;
// i在未命名的命名空间中的声明
namespace {
    int i;  
}
// 二义性错误: i的定义既出现在全局作用域中, 又出现在未嵌套的未命名的命名空间中
i = 10;

未命名的命名空间取代文件中的静态声明:

在标准C++引入命名空间的概念之前,程序需要将名字声明成static的以使其对于整个文件有效。在文件中进行静态声明的做法是从C语言继承而来的。在C语言中,声明为static的全局实体在其所在的文件外不可见。

在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间。

使用命名空间成员

1. 命名空间别名

命名空间的别名使得我们可以为命名空间的名字设定一个短得多的同义词:

namespace foo = tomocat_foo;
// 命名空间的别名也可以指向一个嵌套的命名空间
namespace foo = tomocat::tomocat_foo;

2. using声明

  • 有效范围从using声明的地方开始,一直到using声明所在的作用域结束为止

  • 未加限定的名字只能在using声明所在的作用域以及内层作用域中使用

  • 一条using声明可以出现在全局作用域、局部作用域、命名空间作用域以及类的作用域中;在类的作用域中,这样的声明语句只能指向基类成员(因为派生类只能为那些它可以访问的名字提供using声明)

3. using指示

Tips:实际编程中禁止使用using指示,这样会引入整个命名空间的标识符号从而污染命名空间。

  • using指示以关键字using开始,后面是关键字namespace以及命名空间的名字

  • using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但是不能出现在类的作用域中

  • using声明不同,我们无法控制那些名字是可见的,因为所有名字都是可见的

// 禁止使用using指示: 会污染命名空间
using namespace foo;

4. 头文件与using声明或指示

头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含该头文件的文件中。通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。因此头文件最多只能在它的函数或命名空间中使用using指示或using声明。

Tips:using指示一次性注入某个命名空间中的所有名字,这种用法充满风险:命名空间中所有的成员变得可见了。相比于使用using指示,在程序中对命名空间的每个成员分别使用using声明效果更好,这样可以减少注入到命名空间中的名字数量。using指示也并非一无是处,例如在命名空间本身的实现文件中就可以使用。

重载与命名空间

1. 重载与using声明

using声明语句声明的是一个名字,而非一个特定的函数:

using NS::print(int);   // 错误: 不能指定形参列表
using NS::print;        // 正确: using声明只声明一个名字

我们为函数书写using声明时,该函数的所有版本都被引入到当前作用域中。

2. 跨越多个using指示的重载

如果存在多个using指示,则来自每个命名空间的名字都会成为候选函数的一部分:

namespace AW {
    int print(int);
}
namespace Primer {
    double print(double);
}

// using指示从不同的命名空间中创建了一个重载函数集合
using namespace AW;
using namespace Primer;
long double print(long double);
int main() {
    print(1);   // 调用AW::print(int)
    print(3.1); // 调用Primer::print(double)
    return 0;
}