条款28: 划分全局名字空间
全局空间最大的问题在于它本身仅有一个。在大的软件项目中,经常会有不少人把他们定义的名字都放在这个单一的空间中,从而不可避免地导致名字冲突。例如,假设library1.h定义了一些常量,其中包括:
const double LIB_VERSION = 1.204;
类似的,library2.h也定义了:
const int LIB_VERSION = 3;
很显然,如果某个程序想同时包含library1.h和library2.h就会有问题。对于这类问题,你除了嘴里骂几句,或给作者发报复性邮件,或自己编辑头文件来消除名字冲突外,也没其它什么办法。
但是,作为程序员,你可以尽力使自己写的程序库不给别人带来这些问题。例如,可以预先想一些不大可能造成冲突的某种前缀,加在每个全局符号前。当然得承认,这样组合起来的标识符看起来不是那么令人舒服。
另一个比较好的方法是使用C++ namespace。namespace本质上和使用前缀的方法一样,只不过避免了别人总是看到前缀而已。所以,不要这么做:
const double sdmBOOK_VERSION = 2.0; // 在这个程序库中,
// 每个符号以"sdm"开头
class sdmHandle { ... };
sdmHandle& sdmGetHandle(); // 为什么函数要这样声明?
// 参见条款47
而要这么做:
namespace sdm {
const double BOOK_VERSION = 2.0;
class Handle { ... };
Handle& getHandle();
}
用户于是可以通过三种方法来访问这一名字空间里的符号:将名字空间中的所有符号全部引入到某一用户空间;将部分符号引入到某一用户空间;或通过修饰符显式地一次性使用某个符号:
void f1()
{
using namespace sdm; // 使得sdm中的所有符号不用加
// 修饰符就可以使用
cout << BOOK_VERSION; // 解释为sdm::BOOK_VERSION
...
Handle h = getHandle(); // Handle解释为sdm::Handle,
// getHandle解释为sdm::getHandle
...
}
void f2()
{
using sdm::BOOK_VERSION; // 使得仅BOOK_VERSION不用加
// 修饰符就可以使用
cout << BOOK_VERSION; // 解释为
// sdm::BOOK_VERSION
...
Handle h = getHandle(); // 错误! Handle和getHandle
// 都没有引入到本空间
...
}
void f3()
{
cout << sdm::BOOK_VERSION; // 使得BOOK_VERSION
// 在本语句有效
...
double d = BOOK_VERSION; // 错误! BOOK_VERSION
// 不在本空间
Handle h = getHandle(); // 错误! Handle和getHandle
// 都没有引入到本空间
...
}
(有些名字空间没有名字。这种没命名的名字空间一般用于限制名字空间内部元素的可见性。详见条款M31。)
名字空间带来的最大的好处之一在于:潜在的二义不会造成错误(参见条款26)。所以,从多个不同的名字空间引入同一个符号名不会造成冲突(假如确实真的从不使用这个符号的话)。例如,除了名字空间sdm外,假如还要用到下面这个名字空间:
namespace AcmeWindowSystem {
...
typedef int Handle;
...
}
只要不引用符号Handle,使用sdm和AcmeWindowSystem时就不会有冲突。假如真的要引用,可以明确地指明是哪个名字空间的Handle:
void f()
{
using namespace sdm; // 引入sdm里的所有符号
using namespace AcmeWindowSystem; // 引入Acme里的所有符号
... // 自由地引用sdm
// 和Acme里除Handle之外
// 的其它符号
Handle h; // 错误! 哪个Handle?
sdm::Handle h1; // 正确, 没有二义
AcmeWindowSystem::Handle h2; // 也没有二义
...
}
假如用常规的基于头文件的方法来做,只是简单地包含sdm.h和acme.h,这样的话,由于Handle有多个定义,编译将不能通过。
名字空间的概念加入到C++标准的时间相对较晚,所以有些人会认为它不太重要,可有可无。但这种想法是错误的,因为C++标准库(参见条款49)里几乎所有的东西都存在于名字空间std之中。这可能令你不以为然,但它却以一种直接的方式影响到你:这就是为什么C++提供了那些看起来很有趣的、没有扩展名的头文件,如<iostream>, <string>等。详细介绍参见条款49。
由于名字空间的概念引入的时间相对较晚,有些编译器可能不支持。就算是这样,那也没理由污染全局名字空间,因为可以用struct来近似实现namespace。可以这样做:先创建一个结构用以保存全局符号名,然后将这些全局符号名作为静态成员放入结构中:
// 用于模拟名字空间的一个结构的定义
struct sdm {
static const double BOOK_VERSION;
class Handle { ... };
static Handle& getHandle();
};
const double sdm::BOOK_VERSION = 2.0; // 静态成员的定义
现在,如果有人想访问这些全局符号名,只用简单地在它们前面加上结构名作为前缀:
void f()
{
cout << sdm::BOOK_VERSION;
...
sdm::Handle h = sdm::getHandle();
...
}
但是,如果全局范围内实际上没有名字冲突,用户就会觉得加修饰符麻烦而多余。幸运的是,还是有办法来让用户选择使用它们或忽略它们。
对于类型名,可以用类型定义(typedef)来显式地去掉空间引用。例如,假设结构s(模拟的名字空间)内有个类型名T,可以这样用typedef来使得T成为S::T的同义词:
typedef sdm::Handle Handle;
对于结构中的每个(静态)对象X,可以提供一个(全局)引用X,并初始化为S::X:
const double& BOOK_VERSION = sdm::BOOK_VERSION;
老实说,如果读了条款47,你就会不喜欢定义一个象BOOK_VERSION这样的非局部静态对象。(你就会用条款47中所介绍的函数来取代这样的对象)
处理函数的方法和处理对象一样,但要注意,即使定义函数的引用是合法的,但代码的维护者会更喜欢你使用函数指针:
sdm::Handle& (* const getHandle)() = // getHandle是指向sdm::getHandle
sdm::getHandle; // 的const 指针 (见条款21)
注意getHandle是一个常指针。因为你当然不想让你的用户将它指向别的什么东西,而不是sdm::getHandle,对不对?
(如果真想知道怎么定义一个函数的引用,看看下面:
sdm::Handle& (&getHandle)() = // getHandle是指向
sdm::getHandle; // sdm::getHandle的引用
我个人认为这样的做法也很好,但你可能以前从没见到过。除了初始化的方式外,函数的引用和函数的常指针在行为上完全相同,只是函数指针更易于理解。)
有了上面的类型定义和引用,那些不会遭遇全局名字冲突的用户就会使用没有修饰符的类型和对象名;相反,那些有全局名字冲突的用户就会忽略类型和引用的定义,代之以带修饰符的符号名。还要注意的是,不是所有用户都想使用这种简写名,所以要把类型定义和引用放在一个单独的头文件中,不要把它和(模拟namespace的)结构的定义混在一起。
struct是namespace的很好的近似,但实际上还是相差很远。它在很多方面很欠缺,其中很明显的一点是对运算符的处理。如果运算符被定义为结构的静态成员,它就只能通过函数调用来使用,而不能象常规的运算符所设计的那样,可以通过自然的中缀语法来使用:
// 定义一个模拟名字空间的结构,结构内部包含Widgets的类型
// 和函数。Widgets对象支持operator+进行加法运算
struct widgets {
class Widget { ... };
// 参见条款21:为什么返回const
static const Widget operator+(const Widget& lhs,
const Widget& rhs);
...
};
// 为上面所述的Widge和operator+
// 建立全局(无修饰符的)名称
typedef widgets::Widget Widget;
const Widget (* const operator+)(const Widget&, // 错误!
const Widget&); // operator+不能是指针名
Widget w1, w2, sum;
sum = w1 + w2; // 错误! 本空间没有声明
// 参数为Widgets 的operator+
sum = widgets::operator+(w1, w2); // 合法, 但不是
// "自然"的语法
正因为这些限制,所以一旦编译器支持,就要尽早使用真正的名字空间。