Part IV: Advanced Topics
Chapter 18. Tools for Large Programs
18.2 命名空间
大型程序往往使用独立开发的库。这些库还常常定义大量全局名字,比如类、函数和模板等。
当一个应用程序使用来自许多不同供应商的库时,出现一些名字冲突几乎是不可避免的。若库将名字放在全局命名空间中,会导致命名空间污染 (namespace pollution)。
传统上,程序员通过使用很长的名字定义全局实体来避免命名空间污染。这些名字通常包含一个前缀,表示定义该名字的库:
class cplusplus_primer_Query { ... };
string cplusplus_primer_make_plural(size_t, string&);
命名空间 (namespace) 提供了一种更加可控的机制来防止名字冲突。命名空间分割了全局命名空间;命名空间就是一个作用域。通过在命名空间中定义库的名称,库的作者(和用户)可以避免全局名字中固有的限制。
命名空间定义
命名空间定义以关键字 namespace
开始,后面跟着命名空间的名字。命名空间名字后面是一系列声明和定义,由花括号括起来。可出现在全局作用域中的任何声明都可以放在命名空间中:类、变量(及其初始化)、函数(及其定义)、模板和其他命名空间等。
namespace cplusplus_primer {
class Sales_data { / * ... * /};
Sales_data operator+(const Sales_data&, const Sales_data&);
class Query { /* ... */ };
class Query_base { /* ... */};
} // like blocks, namespaces do not end with a semicolon
命名空间名字在定义命名空间的作用域内必须是唯一的。命名空间可以定义在全局作用域内,也可以定义在另一个命名空间内。但不能定义在函数或类中。
注:命名空间作用域不以分号结尾。
每个命名空间都是一个作用域
因为不同的命名空间引出不同的作用域,所以不同的命名空间可以有同名的成员。
命名空间中定义的名字可以由命名空间的其他成员直接访问,包括嵌套在这些成员中的作用域。命名空间之外的代码必须指出定义此名字的命名空间:
cplusplus_primer::Query q = cplusplus_primer::Query("hello");
命名空间可以是不连续的
与其他作用域不同,命名空间可以定义在几个不同的部分。编写命名空间定义:
namespace nsp {
// declarations
}
上面的代码可能是定义了一个名为 nsp 的新的命名空间,也可能是在已存在的命名空间中添加新成员。
如果名字 nsp 不是指先前定义的命名空间,则创建一个新命名空间,名为 nsp。否则,此定义将打开一个已存在的命名空间,并向该命名空间中添加声明。
命名空间定义可以是不连续的,根据这一事实,我们可以从几个独立的接口和实现文件组成命名空间。因此,可以使用与管理自己的类和函数定义相同的方式来组织命名空间:
- 命名空间中可以定义类、以及作为类接口的函数和对象的声明,命名空间的这些成员放入头文件中。使用这些命名空间成员的文件可以包含这些头文件。
- 命名空间成员的定义可以放在单独的源文件中。
以这种方式组织命名空间还满足了各种实体(非内联函数、静态数据成员、变量等)在程序中只能定义一次的要求。通过分离接口和实现,可以确保所需的函数和其他名字只定义一次,但无论何时使用实体,都会看到相同的声明。
☛实践:定义多个不相关类型的命名空间时,应该使用独立的文件来分别表示命名空间定义的每个类型(或每个相关类型的集合)。
定义 Primer 命名空间
使用这种分离接口和实现的策略,可以在几个独立的文件中定义 cplusplus_primer 库。Sales_data 及其相关函数的声明将放在 Sales_data.h 中,第15章 Query 类的声明将放在 Query.h 中,以此类推。相应的实现文件将位于诸如 Sales_data.cc 和 Query.cc 之类的文件中:
// ---- Sales_data.h---
// #includes should appear before opening the namespace
#include <string>
namespace cplusplus_primer {
class Sales_data { /* ... */};
Sales_data operator+(const Sales_data&, const Sales_data&);
// declarations for the remaining functions in the Sales_data interface
}
// ---- Sales_data.cc---
// be sure any #includes appear before opening the namespace
#include "Sales_data.h"
namespace cplusplus_primer {
// definitions for Sales_data members and overloaded operators
}
// ---- user.cc---
// names in the Sales_data.h header are in the cplusplus_primer namespace
#include "Sales_data.h"
int main() {
using cplusplus_primer::Sales_data;
Sales_data trans1, trans2;
// ...
return 0;
}
这种程序组织方式为库的开发人员和用户提供了所需的模块化。
- 每个类仍然被组织到自己的接口和实现文件中。
- 一个类的用户不需要编译与其他类相关的名称。
- 可以对用户隐藏实现细节,同时允许文件 Sales_data.cc 和 user.cc 被编译并链接到一个程序中,且不会引起编译时或链接时错误。
- 库的开发人员可以独立处理每种类型的实现细节。
注意,通常不会在命名空间中放置 #include。如果这样做,头文件中的所有名字将定义为此命名空间的成员。
例如,如果 Sales_data.h 文件在包含 string 头文件之前打开了 cplusplus_primer,那么程序就会出错。这试图定义 std 命名空间嵌套在 cplusplus_primer 中。
定义命名空间成员
假设作用域中存在合适的声明,命名空间内的代码可以使用定义在同一个(或外层的)命名空间内的名字的简写形式:
#include "Sales_data.h"
namespace cplusplus_primer { // reopen cplusplus_primer
// members defined inside the namespace may use unqualified names
std::istream& operator>>(std::istream& in, Sales_data& s) { /* ... */}
}
也可以在命名空间定义之外定义命名空间成员。名字的命名空间声明必须在作用域中,并且定义必须指出名字所属的命名空间:
// namespace members defined outside the namespace must use qualified names
cplusplus_primer::Sales_data
cplusplus_primer::operator+(const Sales_data& lhs, const Sales_data& rhs) {
Sales_data ret(lhs);
// ...
}
尽管命名空间成员可以在其命名空间之外定义,但这种定义必须出现在所属命名空间的外层作用域中。
例如,可以在 cplusplus_primer 命名空间内或全局作用域内定义 Sales_data operator+,但不能在不相关的命名空间中定义该运算符。
模板特例化
模板特例化必须与原始模板定义在同一命名空间中。只要在命名空间内声明了特例化,就可以在命名空间外定义它:
// we must declare the specialization as a member of std
namespace std {
template <> struct hash<Sales_data>;
}
// having added the declaration for the specialization to std
// we can define the specialization outside the std namespace
template <> struct std::hash<Sales_data> {
size_t operator()(const Sales_data& s) const {
return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^ hash<double>()(s.revenue);
}
// other members as before
};
全局命名空间
定义在全局作用域内的名字(即,声明在任何类、函数或命名空间之外的名字)定义在全局命名空间 (global namespace) 中。全局命名空间是隐式地声明,并存在于每个程序中。在全局作用域内定义实体的每个文件(隐式地)都将这些名字添加到全局命名空间中。
作用域运算符可用于指出全局命名空间的成员。因为全局命名空间是隐式的,所以它没有名字。
::member_name
上面的代码表示全局命名空间的成员。
嵌套的命名空间
嵌套的命名空间是定义在另一个命名空间中的命名空间:
namespace cplusplus_primer {
// first nested namespace: defines the Query portion of the library
namespace QueryLib {
class Query { /* ... */ };
Query operator&(const Query&, const Query&);
// ...
}
// second nested namespace: defines the Sales_data portion of the library
namespace Bookstore {
class Quote { /* ... */ };
class Disc_quote : public Quote { /* ... */ };
// ...
}
}
嵌套的命名空间是一个嵌套的作用域,它的作用域嵌套在包含它的命名空间中。嵌套的命名空间名字遵循常规规则:
- 声明在内层命名空间中的名字,会隐藏声明在外层命名空间中相同的名字。
- 定义在内层命名空间中的名字,是该内层命名空间中的局部名字。
- 外层命名空间中的代码只能通过其限定名表示嵌套的命名空间中的名字。
例如,嵌套的命名空间 QueryLib 中声明的类的名字是 cplusplus_primer::QueryLib::Query
内联命名空间
C++11标准引入了一种新的嵌套命名空间,即内联命名空间 (inline namespace)。与普通的嵌套命名空间不同,内联命名空间中的名字可以直接被外层命名空间使用。即,要想访问内联命名空间中的名字,可以只使用其外层命名空间的名字作为前缀。
定义内联命名空间的方式是,在关键字 namespace 前面加上关键字 inline:
inline namespace FifthEd {
// namespace for the code from the Primer Fifth Edition
}
namespace FifthEd { // implicitly inline
class Query_base { /* ... * /};
// other Query-related declarations
}
关键字 inline 必须出现在命名空间的第一次定义中。如果以后重新打开此命名空间,可以写 inline,也可以不写。
未命名的命名空间
未命名的命名空间 (unnamed namespace) 是关键字 namespace 后紧跟大括号括起来的一系列声明。在未命名的命名空间中定义的变量具有静态生存期:它们在第一次使用之前创建,在程序结束时销毁。
若某个头文件定义了未命名的命名空间,则对于包含该头文件的每个文件来说,该命名空间中的名字定义的是不同的实体。
注:与其他命名空间不同,未命名的命名空间仅属特定文件本地所有,不能跨越多个文件。
直接使用未命名的命名空间中定义的名字。不能使用作用域运算符指出未命名命名空间的成员。
定义在未命名的命名空间中的名字的作用域,与定义此命名空间的作用域相同。如果在文件的最外层作用域中定义了未命名的命名空间,则该命名空间中的名字必须与定义在全局作用域中的名字不同:
int i; // global declaration for i
namespace {
int i;
}
// ambiguous: defined globally and in an unnested, unnamed namespace
i = 10;
在所有其他方式中,未命名的命名空间的成员都是常规的程序实体。
未命名的命名空间可以嵌套在另一个命名空间中。如果未命名的命名空间是嵌套的,则使用外层命名空间的名字以正常方式访问其中的名字:
namespace local {
namespace {
int i;
}
}
// ok: i defined in a nested unnamed namespace is distinct from global i
local::i = 42;
未命名的命名空间替代文件中的静态声明
在引入命名空间之前,程序将名字声明为 static,以使它们成为文件的本地名称。文件静态声明 (file statics) 的用法是从 C 继承的。在 C 中,声明为 static 的全局实体在声明它的文件之外是不可见的。
⚠文件 static 声明的用法被C++标准所弃用。应该避免使用文件静态声明,而是使用未命名的命名空间。
使用命名空间成员
使用 namespace_name::member_name 访问命名空间中的成员有点繁琐。有一些方法可以使命名空间成员的使用更容易:using 声明、命名空间别名和 using 指示。
命名空间别名
命名空间别名 (namespace alias) 可用于将较短的同义词与命名空间名字关联起来。
namespace cplusplus_primer { /* ... */ };
上面的命名空间名字可以关联到一个更短的同义词:
namespace primer = cplusplus_primer;
命名空间别名也可以表示一个嵌套的命名空间。
namespace Qlib = cplusplus_primer::QueryLib;
Qlib::Query q;
注:一个命名空间可以有多个同义词或别名。所有别名和原始命名空间名字都可以互换使用。
using 声明:概述
using 声明 (using declaration) 一次只引入一个命名空间成员。
using std::cout;
cout << "hello" << endl;
using 声明中引入的名字遵循常规的作用域规则:
- 它的有效范围从 using 声明的地方开始,到声明所在的作用域的结尾为止。
- 定义在外层作用域中的具有相同名字的实体将被隐藏。
- 未加限定的名字只能在声明它的作用域内及其嵌套的作用域内使用。
- 一旦作用域结束,就必须使用完整的限定名。
using 声明可以出现在全局、局部、命名空间或类作用域中。在类作用域内,这样的声明只能指向基类成员 (§15.5)。
using 指示
using 指示允许我们使用命名空间名字的非限定形式。其中所有的名字都是可见的。
using 指示以关键字 using 开头,后跟关键字 namespace 与命名空间名字。若名字不是已定义的命名空间名字,则程序出错。
using 指示可以出现在全局、局部或命名空间作用域中。它不能出现在类作用域中。
using namespace std;
string s;
cin >> s;
cout << s << endl;
这些指示使特定命名空间中的所有名字可见,而无需添加前缀限定符。从 using 指示的地方到 using指示所在作用域的结尾都可使用简写形式的名字。
⚠如果为应用程序未作控制的命名空间提供 using 指示,会重新引入使用多个库时固有的名字冲突问题。
using 指示与作用域
using 声明将名字放在与 using 声明本身相同的作用域内。好像 using 声明在当前作用域内声明了命名空间成员的别名一样。
using 指示的作用是,将命名空间成员提升到包含命名空间本身和 using 指示的最近作用域中。
using 声明和 using 指示之间的作用域差异直接源于它们的工作方式不同。
- 使用 using 声明,只是使名字在局部作用域内是可直接访问的。
- using 指示使命名空间的全部内容都可用。通常,命名空间可能包含不能出现在局部作用域中的定义。因此,using 指示被视为出现在最近的外层命名空间作用域中。
假设有一个命名空间 A 和一个函数 f,它们都定义在全局作用域内。如果 f 有一个对 A 的 using 指示,那么对 f 来说,就好像 A 中的名字出现在全局作用域中 f 的定义之前的位置:
// namespace A and function f are defined at global scope
namespace A {
int i, j;
}
void f() {
using namespace A; // injects the names from A into the global scope
cout << i * j << endl; // uses i and j from namespace A
// ...
}
using 指示示例
namespace blip {
int i = 16, j = 15, k = 23;
// other declarations
}
int j = 0; // ok: j inside blip is hidden inside a namespace
void manip() {
// using directive; the names in blip are ''added'' to the global scope
using namespace blip; // clash between ::j and blip::j detected only if j is used
++i; // sets blip::i to 17
++j; // error ambiguous: global j or blip::j?
++::j; // ok: sets global j to 1
++blip::j; // ok: sets blip::j to 16
int k = 97; // local k hides blip::k
++k; // sets local k to 98
}
头文件与 using 声明或指示
如果头文件在其顶层作用域中具有 using 指示或声明,则会将名字注入所有包含该头文件的文件中。
通常,头文件应该只定义接口部分的名字,而不是在其自身实现中使用的名字。因此,头文件不应包含using 指示或 using 声明,函数或命名空间内除外。
注意:避免 using 指示
using 指示可能造成的问题:
- 如果应用程序使用多个库,如果使用 using 指示使得这些库中的名字都可见,那么全局命名空间污染问题会重新出现。
- 由 using 指示造成的二义性错误只能在使用了冲突名字的地方才能检测出来 (见 using 指示示例代码中的变量 j)。这种延迟检测意味着在引入特定库很长时间后才会发生冲突。如果程序开始使用该库的新部分,以前未检测到的冲突会出现。
💡Tip:using 指示很有用的一个地方是命名空间本身的实现文件。
类、命名空间与作用域
命名空间中使用的名字的名字查找遵循常规的查找规则:由内向外查找每个作用域。只有在使用点之前已声明、仍处于打开状态的块中的名字才会被考虑:
namespace A {
int i;
namespace B {
int i; // hides A::i within B
int j;
int f1() {
int j; // j is local to f1 and hides A::B::j
return i; // returns B::i
}
} // namespace B is closed and names in it are no longer visible
int f2() {
return j; // error: j is not defined
}
int j = i; // initialized from A::i
}
当类包装在命名空间中时,仍遵循常规的名字查找:当成员函数使用某个名字时,首先在该成员中查找该名字,然后在类(包括基类)中查找,然后在外层作用域中查找:
namespace A {
int i;
int k;
class C1 {
public:
C1(): i(0), j(0) { } // ok: initializes C1::i and C1::j
int f1() { return k; } // returns A::k
int f2() { return h; } // error: h is not defined
int f3();
private:
int i; // hides A::i within C1
int j;
};
int h = i; // initialized from A::i
}
// member f3 is defined outside class C1 and outside namespace A
int A::C1::f3() { return h; } // ok: returns A::h
💡Tip:可以从函数的限定名推断出在查找名字时检查作用域的顺序。按照限定名相反的顺序查找作用域。
限定符 A::C1::f3 指出要查找的类作用域和命名空间作用域的相反顺序。首先查找函数 f3 的作用域,然后查找其外层类 C1 的类作用域,接着查找命名空间 A 的作用域,最后检查包含 f3 定义的作用域。
实参相关的查找与类类型的形参
std::string s;
std::cin >> s;
上面的调用等价于
operator>>(std::cin, s);
这个 operator>> 函数由 string 库定义,而 string 库又定义在 std 命名空间中。然而,可以在没有 std:: 限定符和 using 声明的情况下调用 operator>>。
可以直接访问输出运算符是因为,对于规则:定义在命名空间中的名字是隐藏的,有一个重要的例外。
当传递一个类类型的对象给一个函数时,编译器除了常规的作用域查找外,还会查找定义了实参类的命名空间。这一例外也适用于传递指向类类型的指针或引用的调用。
在本例中,当编译器看到 operator>> 的“调用”时,它会在当前作用域及其外层作用域中查找匹配的函数。
此外,由于 >> 表达式具有类类型形参,所以编译器还会在定义 cin 和 s 类型的命名空间中查找。因此,对于这个调用,编译器在 std 命名空间中查找,该命名空间定义了 istream 和 string 类型。当它查找 std 时,编译器会找到 string 输出运算符函数。
查找规则中的这个异常让我们,可以使用概念上属于类接口部分的非成员函数,而不需要单独的 using 声明。
友元声明和实参相关查找
当类声明一个友元时,友元声明不会使该友元可见。然而,如果未声明的类或函数的第一次命名是在友元声明中,那么它会被认为是最近的外层命名空间的成员。将此规则与实参相关的查找结合起来可能会带来意外效果:
namespace A {
class C {
// two friends; neither is declared apart from a friend declaration
// these functions implicitly are members of namespace A
friend void f2(); // won't be found, unless otherwise declared
friend void f(const C&); // found by argument-dependent lookup
};
}
上面代码中,f 和 f2 都是命名空间 A 的成员。通过实参相关的查找,即使 f 没有其他声明,也可以调用 f:
int main() {
A::C cobj;
f(cobj); // ok: finds A::f through the friend declaration in A::C
f2(); // error: A::f2 not declared
}
因为 f 接受一个类类型的实参,并且 f 隐式地声明在与 C 同一命名空间中,所以在调用 f 时会找到 f。因为 f2 没有形参,所以找不到它。
重载与命名空间
命名空间对函数匹配有两个影响。其中之一是:using 声明或 using 指示可以向候选集中添加函数。另一个则要微妙得多。
实参相关的查找与重载
具有类类型实参的函数的名字查找,包括定义每个实参的类的命名空间。
这个规则还影响我们如何确定候选集。在每个实参类(以及实参类的基类)所属的命名空间中,搜索候选函数。这些命名空间中,如果有函数与被调用函数同名,那么这些函数将添加到候选集。即使在调用点不可见,也会添加这些函数:
namespace NS {
class Quote { /* ... */ };
void display(const Quote&) { /* ... */ }
}
// Bulk_item's base class is declared in namespace NS
class Bulk_item : public NS::Quote { /* ... */ };
int main() {
Bulk_item book1;
display(book1);
return 0;
}
传递给 display 的实参具有类类型 Bulk_item。display 调用的候选函数集不仅包括调用 display 的作用域中已声明的函数,而且包括声明 Bulk_item 及其基类 Quote 的命名空间中的函数。命名空间 NS 中声明的函数 display(const Quote&) 被添加到候选函数集中。
重载与 using 声明
using 声明语句声明的是一个名字,而不是一个特定函数。
using NS::print(int); // error: cannot specify a parameter list
using NS::print; // ok: using declarations specify names only
当为一个函数编写 using 声明时,该函数的所有版本都会被引入当前作用域内。
using 声明包含所有版本,以确保不违反命名空间的接口。
using 声明引入的函数重载该 using 声明所在作用域中已存在的同名函数的声明。
如果 using 声明出现在局部作用域中,则这些名字将隐藏外部作用域中已存在的该名字的声明。
如果 using 声明在一个作用域中引入了一个函数,而该作用域中已有一个同名函数,且形参列表相同,则 using 声明是错误的。否则,using 声明定义给定名字的其他重载实例,候选函数集增加了一个函数。
重载与 using 指示
using 指示将命名空间成员提升到其外层作用域中。如果命名空间函数与该命名空间所在作用域中声明的函数同名,则命名空间成员将被添加到重载集合中:
namespace libs_R_us {
extern void print(int);
extern void print(double);
}
// ordinary declaration
void print(const std::string &);
// this using directive adds names to the candidate set for calls to print:
using namespace libs_R_us;
// the candidates for calls to print at this point in the program are:
// print(int) from libs_R_us
// print(double) from libs_R_us
// print(const std::string &) declared explicitly
void fooBar(int ival) {
print("Value: "); // calls global print(const string &)
print(ival); // calls libs_R_us::print(int)
}
与 using 声明的工作方式不同,如果 using 指示引入的函数与已有的函数具有相同的形参,则不是错误。除非调用函数时不指定是从命名空间中还是从当前作用域中调用函数,这才会发生错误。
跨越多个 using 指示的重载
如果存在多个 using 指示,则每个命名空间中的名字将成为候选集的一部分:
namespace AW {
int print(int);
}
namespace Primer {
double print(double);
}
// using directives create an overload set of functions from different namespaces
using namespace AW;
using namespace Primer;
long double print(long double);
int main() {
print(1); // calls AW::print(int)
print(3.1); // calls Primer::print(double)
return 0;
}
在全局作用域内,函数 print 的重载集合包括函数 print(int)、print(double) 和 print(long double)。