C++ 嵌套 namespace 解析

3,204 阅读5分钟

写在前面:

  • 网上似乎找不到解释清楚嵌套 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
    • ... 如法炮制

如上流程有几点需要注意:

  1. 在整棵空间树中,从根空间到当前空间的所有空间为“路径空间”,与路径空间直接相连的空间,都可以直接写在相对引用路径的首位。如上例代码中,D 空间中的引用链可以将 C,Slot 写在首位,即 C::n, Slot::n
  2. 路径空间中的符号可以直接引用,不需要加 specifier。如 D 中可以直接引用 System, Sentinel 中的符号;
  3. 相对引用路径上所有空间名都要在编译时解析,但只有首位空间名在未找到时可以到父节点继续找,对于非首位的空间,未找到直接报错;
  4. 对于任意空间,根空间始终在路径空间中(且在路径头部),因此最保险的写法是使用“绝对引用路径”,这样一定不会出现空间/符号解析错误,但代码变得比较冗长。事实上,并不会区分相对与绝,解析流程中找到根空间去了就可视为绝对路径。

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::CSentinel::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,不同编译器解析规则会不会不一样呢