无名命名空间与内联命名空间
C++ 支持两种命名空间的变体,至少值得了解。我们不会在此基础上继续深入,因此本节内容可视作选学。
一、无名(匿名)命名空间
无名命名空间(亦称匿名命名空间)是指未指定名称的命名空间,定义方式如下:
#include <iostream>
namespace // 无名命名空间
{
void doSomething() // 仅可在本文件内访问
{
std::cout << "v1\n";
}
}
int main()
{
doSomething(); // 无需命名空间前缀即可调用
return 0;
}
输出:
v1
无名命名空间中的所有内容被视为父命名空间的一部分。因此,尽管 doSomething()
定义于无名命名空间,但其本身可从父命名空间(此处为全局命名空间)直接访问,这也是 main()
无需限定符即可调用 doSomething()
的原因。
仅此一点,似乎使无名命名空间显得多余。然而,无名命名空间的另一作用是:其内部所有标识符均被视为具有内部链接属性,即无名命名空间的内容无法在其定义文件之外被看见。
对函数而言,这等效于将无名命名空间内的所有函数声明为 static
。以下程序在效果上与上述程序完全一致:
#include <iostream>
static void doSomething() // 仅可在本文件内访问
{
std::cout << "v1\n";
}
int main()
{
doSomething(); // 无需命名空间前缀即可调用
return 0;
}
当需要确保大量内容限定于某个翻译单元时,通常使用无名命名空间;与逐一将声明标记为 static
相比,将此类内容聚集在单一无名命名空间更为便捷。无名命名空间还能使后续课程将讨论的程序自定义类型限定于翻译单元,而这是无可替代的机制。
提示
若追求极致,可采取反向策略——将所有不打算显式导出/对外暴露的内容放入无名命名空间。
无名命名空间一般不应出现在头文件中。SEI CERT(规则 DCL59-CPP)提供了若干良好示例,阐明其原因。
最佳实践
若需将内容限定于翻译单元,优先使用无名命名空间。
避免在头文件中使用无名命名空间。
二、内联命名空间
观察以下程序:
#include <iostream>
void doSomething()
{
std::cout << "v1\n";
}
int main()
{
doSomething();
return 0;
}
输出:
v1
非常简单,对吧?
假设您对 doSomething()
不满意,想以某种方式改进其行为,但这可能破坏使用旧版本的既有程序。如何处理?
一种方法是为新函数另起名称,然而多次更迭后,您可能得到一组几乎同名却版本递增的函数(doSomething
、doSomething_v2
、doSomething_v3
,等等)。
另一种方案是使用内联命名空间。内联命名空间通常用于版本化内容。与无名命名空间类似,内联命名空间内的任何声明均被视为父命名空间的一部分;但与无名命名空间不同的是,内联命名空间不影响链接属性。
定义内联命名空间需使用 inline
关键字:
#include <iostream>
inline namespace V1 // 声明名为 V1 的内联命名空间
{
void doSomething()
{
std::cout << "V1\n";
}
}
namespace V2 // 声明名为 V2 的普通命名空间
{
void doSomething()
{
std::cout << "V2\n";
}
}
int main()
{
V1::doSomething(); // 调用 V1 版 doSomething()
V2::doSomething(); // 调用 V2 版 doSomething()
doSomething(); // 调用内联版 doSomething()(即 V1)
return 0;
}
输出:
V1
V2
V1
在本例中,调用 doSomething()
将获得 V1(内联版本)。若需使用新版本,可显式调用 V2::doSomething()
。这保留了既有程序的功能,同时允许新程序利用更新/更优的变体。
反之,若想默认推广新版本:
#include <iostream>
namespace V1 // 声明名为 V1 的普通命名空间
{
void doSomething()
{
std::cout << "V1\n";
}
}
inline namespace V2 // 声明名为 V2 的内联命名空间
{
void doSomething()
{
std::cout << "V2\n";
}
}
int main()
{
V1::doSomething(); // 调用 V1 版 doSomething()
V2::doSomething(); // 调用 V2 版 doSomething()
doSomething(); // 调用内联版 doSomething()(即 V2)
return 0;
}
输出:
V1
V2
V2
在此例中,所有调用 doSomething()
的代码默认获得 v2 版(更新更优)。仍需旧行为的用户可显式调用 V1::doSomething()
。这意味着现有程序若需 V1 版,需全局将 doSomething
替换为 V1::doSomething
,但若函数命名规范,此操作通常不成问题。
三、混合使用内联与无名命名空间(选学)
命名空间可同时为内联与无名:
#include <iostream>
namespace V1 // 声明名为 V1 的普通命名空间
{
void doSomething()
{
std::cout << "V1\n";
}
}
inline namespace // 声明内联无名命名空间
{
void doSomething() // 具有内部链接
{
std::cout << "V2\n";
}
}
int main()
{
V1::doSomething(); // 调用 V1 版 doSomething()
// 本例无 V2 命名空间,因此无法使用 V2:: 前缀
doSomething(); // 调用内联版 doSomething()(即匿名版)
return 0;
}
然而,在此情形下,更佳做法是将匿名命名空间嵌套于内联命名空间内。这样既保持匿名命名空间内函数默认具有内部链接的效果,又保留了可显式使用的命名空间名称:
#include <iostream>
namespace V1 // 声明名为 V1 的普通命名空间
{
void doSomething()
{
std::cout << "V1\n";
}
}
inline namespace V2 // 声明名为 V2 的内联命名空间
{
namespace // 匿名命名空间
{
void doSomething() // 具有内部链接
{
std::cout << "V2\n";
}
}
}
int main()
{
V1::doSomething(); // 调用 V1 版 doSomething()
V2::doSomething(); // 调用 V2 版 doSomething()
doSomething(); // 调用内联版 doSomething()(即 V2)
return 0;
}