写在前面:
- 网上似乎找不到解释清楚嵌套 namespace 解析规则的官方或非官方文档。
- 此文章的所有解释性内容都来自从运行结果归纳,而非从源码逻辑演绎。更可靠的做法是阅读 gcc 源码 gcc.gnu.org/git.html (目测挺难的……)
命名空间组织
C++ 将嵌套 namespace 组织为一棵树。譬如,对于下面的 namespace 声明:
namespace Sentinel {
int n = 2;
namespace Slot{
int n = 16;
namespace A { int n = 32; }
namespace B { int n = 64; }
}
namespace System {
int n = 128;
namespace C { int n = 256; }
namespace D {
int n = 512;
int sn = C::n + 1;
}
int sn = Slot::n + 1;
}
}
C++ 编译时将其解析为如上一棵__树__,每个节点为一个命名空间(下称“空间”),内部包含若干自定义符号(可以是变量、类、函数等)。每个空间中的符号名无重复,但不同空间可能有重复的符号名。如上示例中,每个空间中都定义了
n ,但只要在引用 n 处加上空间名前缀,编译时就可以唯一定位到指定空间中的变量。
允许将一个空间定义在不同处或不同文件中,每处定义都要嵌套同样层次结构的父空间与大括号,否则就不算同一空间。
解析流程
由于同样的符号名可能在多个空间中都有(重名),于是,将代码中的符号定位到哪个空间就成为编译过程中的一个关键问题。引用空间中的符号解析流程如下:
- 对于空间外(如
main函数中)的引用,在符号前加上所有祖先空间名访问之,即namespace-name::namespace-specifier::symble-name - 对于空间中的引用,先定位到该空间,再定位到符号。定位空间时,先在当前空间中找子空间,找不到时,再当前空间的父空间中找父空间的子空间。如
U空间中引用X::Y::sym(称为“相对引用路径”)- 寻找 U 的子空间 X
- 寻找
U::X的子空间U::X::Y- 寻找
U::X::Y空间中的的符号sym - 找不到符号
U::X::Y::sym,报错no member named 'sym' in namespace 'U::X::Y'
- 寻找
- 寻找
- 找不到子空间
U::X::Y,报错undeclared identifier 'Y' in namespace 'U::X' U中找不到子空间U::X,设U的父空间为V,寻找空间V::X- ... 如法炮制
- V 中找不到子空间
V::X,设 V 的父空间为 W ,寻找空间W::X - ... 如法炮制
- 寻找 U 的子空间 X
如上流程有几点需要注意:
- 在整棵空间树中,从根空间到当前空间的所有空间为“路径空间”,与路径空间直接相连的空间,都可以直接写在相对引用路径的首位。如上例代码中,D 空间中的引用链可以将 C,Slot 写在首位,即
C::n,Slot::n; - 路径空间中的符号可以直接引用,不需要加 specifier。如
D中可以直接引用 System, Sentinel 中的符号; - 相对引用路径上所有空间名都要在编译时解析,但只有首位空间名在未找到时可以到父节点继续找,对于非首位的空间,未找到直接报错;
- 对于任意空间,根空间始终在路径空间中(且在路径头部),因此最保险的写法是使用“绝对引用路径”,这样一定不会出现空间/符号解析错误,但代码变得比较冗长。事实上,并不会区分相对与绝,解析流程中找到根空间去了就可视为绝对路径。
Example
继续考虑上面的例子,将第13行做如下修改, Sentinel::System::D::sn 实际值如下:
13: int sn = n + 1; // sn = 513
13: int sn = C::n + 1; // sn = 257
13: int sn = Slot::n + 1; // sn = 17
13: int sn = Sentinel::Slot::n + 1; // sn = 17, 同上
13: int sn = Slot::A::n + 1; // sn = 33
13: int sn = Slot::C::n + 1; // Error: undeclared identifier 'C' in namespace 'Slot'
隐藏的坑
不难发现上面的解析流程有个坑:对于挂在树上的两个不同位置的空间,从另一个节点去引用时,相对路径可能发生冲突。考虑在上例基础上添加了一个空间 C :
namespace Sentinel {
int n = 2;
namespace C { int n = 4, m = 8; } // Add this namespace
namespace Slot{
int n = 16;
namespace A { int n = 32; }
namespace B { int n = 64; }
}
namespace System {
int n = 128;
namespace C { int n = 256; }
namespace D {
int n = 512;
int sn = C::n + 1;
int sm = C::m + 1;
}
int sn = Slot::n + 1;
}
}
对于两个红框空间,如果从外部引用,(绝对)引用路径分别为 Sentinel::C 与 Sentinel::System::C ;如果从 D 引用,相对引用路径都为 C 。那么,这两个空间到底是不是一个空间呢?
当然不是同一个空间,事实上编程时我们也不需纠结两个空间到底是不是同一个,只需要确定某路径定位到唯一的变量即可。但不幸的是,如上情况会出现变量冲突。
考虑代码第 14、15 行:
• 第 14 行引用了冲突空间中都定义了的变量,按照上述解析流程,距离当前空间(D)更近的祖先节点(Sentinel::System::C)被优先解析,得 sn = 257
• 第 15 行 m 未在 Sentinel::System::C 中定义,由于这个空间被优先解析,故编译时报错 no member named 'm' in namespace 'C',哪怕 Sentinel::C 中定义了 m ,但由于这个空间在 D 中不被解析,故定义无效。
• 如果去掉第 11 行 Sentinel::C 空间的定义,空间树中对 D 来说不再有冲突的 C 空间,任何对 C 的引用都会解析到 Sentinel::C ,可正常引用 m,n 。
PS
- 还是挺复杂的,一般不建议命名空间嵌套太深或者出现冲突空间名……
- 以上示例都用的 clang,不同编译器解析规则会不会不一样呢