加载中...
为什么 C::C::C::C::foo() 能编译成功?
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
课文封面

这个案例,至少牵涉到 C++ 中的 以下 知识点:

  • “Unqualified name lookup / 未限定的名字查找”
  • “Qualified name lookup / 有限定的名字查找”
  • “Injected-class-name/ ‘注入式’类名称 ”
  • “Injected-class-name and constructors / ‘注入式’类名称 和 构造函数 的关系 ”
  • “Elaborated type specifiers / 详细的类型说明符 ”

0 问题

有人问, 以下代码为什么能通过编译:

class Entity { public: static void foo() {} }; int main() { Entity::Entity::Entity::Entity::Entity::foo(); }

主要是这一行:

Entity::Entity::Entity::Entity::Entity::foo();

为什么是合法的?


Entity::Entity::Entity::Entity::Entity::Entity::foo() 竟然编译成功?这一切的背后,是人性的扭曲,还是道德的沦丧? 敬请关注今晚八点 CPPTV 12 频道,让我们跟随镜头走进厚厚的C++标准文档……

这个案例,至少牵涉到 C++ 中的 以下 知识点:

  • “Unqualified name lookup / 未限定的名字查找”
  • “Qualified name lookup / 有限定的名字查找”
  • “Injected-class-name/ ‘注入式’类名称 ”
  • “Injected-class-name and constructors/‘注入式’类名称和构造函数的关系 ”
  • “Elaborated type specifiers / 详细的类型说明符 ”

如果被限定的名称,最终发现是一个C++基础类型 (bool, int, char 等),那还得牵涉出: "pseudo-constructor-name 或 pseudo-destructor-name / 伪构造或伪析构名字 ” 等等

1. 从特例 A::A 的解析说起

首先我们从 “Qualified name lookup ” 的一特例说起: A::A 表示什么?

如果 A 是一个类 (或结构,或相应的别名,以下均只以“类”代表 ),并且在上下文限定中,查找 A 符号的过程无须过滤掉函数名称, 那么, A::A 就只能表示 A 的构造函数的名字。比如:

struct A {}; using T = A::A; // 编译失败 int main() {}

问: using 这行代码能编译通过吗?
答:不行的,编译将得到类似 “A::A 是构造函数,不是类型”这样的出错信息。如下图:

A::A是构造函数,不是类型

这是一个特例,如果 A 有一个基类叫 B,则 A::B 立刻表示 一个类型(即 B ),如:

struct B {}; struct A : B {}; using T = A::B; // 编译成功 int main() {}

2. “Elaborated type specifiers” 和 “Qualified name lookup”

那要怎么让 A::A 请示 struct A 这个类型呢?这就要用上 “Elaborated type specifiers”。方法是加上 typename 或 class 或 struct 之一。只要加上三者之一即可,并不需要和 A 倒底是 struct 还是 class 对应上。因为加上这三者,就是为了很 “elaborated ”地表示:这是一个类型。

我们就选 “typename”,因为它看起来如此直观:“类型名”:

struct A {}; using T = typename A::A; // 编译成功 int main() {}

这时候生效的是C++的哪一条规则呢? 答,“Qualified name lookup / 有限定的名字查找” 。先从操作符 “:: ”说起。

“ :: ”被称为 “ scope resolution operator ” ,用于限定一个符号的查找范围,它的右边的符号,直观上,我们会叫它 是“查找目标”,它的左边,直观上会叫它 “查找范围”。比如 C::F ,很容易推想:编译器 的查找过程 就是 在 C 的有效范围里,查找 符号 F 是什么东东。如果是C是一个class/struct,那查找范围还可扩大到 它的基类(如果有)。事实上,C++标准也确实规定了:解析 C::F 时,编译器必须先解析 C ,再解析 F……然而,C++标准又规定了,在解析 C::F 中的C时,应该加某种优先级,跳过 “Unqualified name lookup”,以尝试将 C 解析为 某个类名 (class\struct\union) 、namespace 或 枚举名 (本质也是类型名)。比如,下面的代码肯定编译失败:

struct a { static int i; }; int a::i; int main() { int a; a a1; // 编译失败。 }

“a a1;” 这行中的 a ,没有加任何空间限定,所以在查找 它 是什么时,用的是 “Unqualified name lookup / 未限定的名字查找”,其方法就是就近往前找,于是找到 a 是一个 int 变量(而不是一个类型),自然, “a a1;” 语法错误。

怎么让编译器知道我们希望写在这里的 a 是一个 类(或结构)呢?直观的想法当然是它加上限定:

struct a { static int i; }; int a::i; int main() { int a; ::a a1; // 编译成功。 }

a 被加限定( :: 左边为空白,表示全局),而全局名字空间里,确实有个 struct a 。

但有意思的是, 想让编译器视 a 视为一种类型,既可以为它加限定,也可以让它去限定别的符号,比如:

struct a { static int i; }; int a::i; int main() { int a; a::i = 666; // 编译成功 }

编译器解析过程如下:看到 “a::i”中有个 “::”,于是采用 “Qualified name lookup ”,于是此处的 a 不受上面 的 “int a”影响,优先 将它当成 类类型、namespace、或enum 查找……于是找到 struct a 。而 struct a 里面正好有个静态成员 i,满足 “a::i = 666”的操作……

注意,这个名字查找过程中,“a::i”基本被视为一个整体。否则,如果按要求,一定要先解析出 :: 的左边的 a 是什么的话,那由于 它本身 未再有 新的限定 (它的左边不再有 :: ),那么,它就应该被解析为 一个 整数变量;然后,整数变量后面接 “::i”,显然是错误的语义,编译失败——但实际情况是,编译成功了。因为,加了“::”后,“Qualified name lookup ”的优先级高于“Unqualified name lookup”了。

不过,这个“优先级”是有限的。如果两条或更多 “Qualified name lookup”的限定规则时,此时大家都是“有身份/Qualified”的人,谁也不比谁优先,于是编译器就只能报错了,比如:

namespace n1 { struct a { static int i; }; int a::i; } // namespace n1 int main() { int a; a::i = 666; // 编译失败,正确做法: n1::a::i = 666; 或 上面 加 using namespace n1; }

a::i 仍然使用带限定的名字查找法,仍然优先于 int a 中的 “a”的作用;但它却找不到合适的 a了:现在 struct a 位于 另一个“qualified / 有限制的” 的空间范围内: n1 。此时,要么加上 using namespace n1 ,要么明确使用 n1::a::i 。

3. class 范围内的自动 “Qualified name lookup”

对于“Qualified name lookup”, 我们还有个补充:在 一个类(假设类名为 C)的范围里写代码,此时对符号的查找,哪怕不加 “ C::”限定,也是会在 “Unqualified name lookup” 失败之后,主动加上“C::”作为 “Qualified”,再找一次的。

距离扣题,还有最后那么几步……上面我们讲了规则是什么什么,但没有讲为什么有这些规则;所以我们还需要一些规则必要性解释及“有某规则和没有某规则”的对比:

很早很早以前,那时的C++的class内,是不会自动采用 “Qualified name lookup”再查找一次的,所以:

/* 曾经,约30多年前的C++, 这个类定义会编译失败 */ class Coo { char c; public: void M() { c = 'A'; // 编译成功, 往前找 c ,发现它是 char a = 1; // 编译失败,a 是什么? m(); // 编译失败, m 是什么? T t; // 编译失败,T 是什么? } private: typedef int T; void m(); int a; };

「纯猜测」 C++之父写的示例代码,到现在也常常将 私有成员 放在最前面,我怀疑他并不是为了省写一次“private”,我怀疑他就是习惯了之前的查找法。

注意上面的表达,当没有明确写 “::”时,在类中也仍然优先使用“Unqualified name lookup”,所以这才有C++程序员都熟悉的,非常经典的某种写法:

class Coo { public: Coo(int a, int b) : a(a), b(b) {} private: int a, b; };

以其中的 “a(a)”为例,表意是 用括号中(右边)的 a 初始化 括号外(左边)的 a 。两个 a 都不带 “::” 限定,因此都优先使用 “Unqualified name lookup”,而后 括号中的 a 解析成功,括号外的 a ,因为要作为初始化的目标,所以不可能构造函数的入参中的 a ,于是改用 “Coo::a ”进行“Qualified name lookup”,这回成功了。

4. 解题

现在来看 题目中的 Entity :

class Entity { public: static void foo(); }

首先,尝试用 “特例” A::A 来解释:如果A是一个class/struct/,则A::A 必然用于表示 类A 的构造函数名字这规定,那么题目中的这个写法:

Entity::Entity::Entity::foo();

感觉是不合语法的。因为一开始的 “Entity::Entity” 就应该得到一个 构造函数的名字,而构造函数名字后面再接“::Entity::foo() ……” 是不合语法的。注意,如果没有的最后的 ::foo(),两个或更多的 Entity:: 相连,仍然在表达 一个构造函数。但在语法上,构造函数被不允许被直接调用(析构函数倒是可以),因此,能正确使用 A::A::A::A::A 这样的写法,基本就是在构造函数的定义\实现的时候了,比如:

// class Entity 的构造函数实现: Entity::Entity::Entity::Entity::Entity::Entity::Entity() { }

在别的地方这么写,编译器仍然会识别出这是一个构造函数,但它会基于其它规则而报错,比如:

void foo() { // 直接调用 (但可惜构造函数不能直接调用) Entity::Entity(); // 报错:哎呀,不能直接调用 构造函数 }

再如前面使用过的例子:

// 尝试取类型别名 (但人家 Entify 此处不是类型名 ) using T = Entity::Entity::Entity; // 报错:哎呀,Entity 是构造函数的名字,不是类型啦

既然一串的 “Entity::Entity”表示的是 构造函数的名字,那么怎么解释 “Entity::Entity::Entity::foo();” 却通过了编译,并且在运行期正确地执行了静态成员函数 foo() 呢?

首先,我们要证明一下,“XXX::Foo”作为静态成员函数的调用的一方式,前面加的XXX一定是一个类名,而不是构造函数的名字。

其实不证明也可以,因为C++标准规范中讲解 static member function 的调用时,明确就说那个 XXX 是 类名。但,证明一下也不难——

using T = typename Entity::Entity::Entity; //编译通过,T 现在就是类名

加了 typename (或 class、struct)之后,后面的 Entity::Entity::Entity ……就是“ Elaborated type specifiers / 详细的类型说明符 ”,于是它肯定是个类型名 (type specifiers),于是 T现在肯定就是一个类名,事实上就是 class Entity。

然后我们假借 T 来调用 静态成员:

T::foo(); // 成功

由此可证:foo() 前面 的“T::”,就是一个“类型限定”(而不是我们意想天开的构造函数名字)。

有意思(其实超级烦人)的事来了:既然 T 就是 typename Entity::Entity::Entity,而 T::foo(),又能成功编译、运行;那我们为什么一定要取个别名呢?直接这样写不行吗:

typename Entity::Entity::Entity::foo(); // 行吗?不行!

这样写多直观啊!可惜,替换率竟然失效了。这样写编译失败。因为 typename 直接修饰到了 foo(),而 foo() 是函数调用,显然不是一个(位于Entity类内的)类型名称。

那么我们加上括号,强行改变结合率:

(typename Entity::Entity::Entity)::foo(); // 行吗?也不行!

也不行,因为 typename 不是一个操作符,没有优先级这一说。 实际上,编译器(g++/clang)看到 typename 前面有个 左括号,就直接报错了。

显然,我们按照所谓“A::A”的特例,来解释 “Entity::Entity::Entity::foo()”的合法性,是走不通的。并且还不能怪C++标准,只能怪我们自己,因为人家标准说很清楚,是 A::A ,或都 A::A::A,而不我们要解释的,其实是 A::A::F 。最后的符号 是F而不是A,不满足特例。

Entity::Entity::Entity::Entity::Entity::Entity::foo() 竟然编译成功?这一切的背后,原来既不是人性的扭曲,更不是道德的沦丧,而是我们眼花看错了,原来 Entity::Entity::foo 并不符合 C::C 的特例,是我们自己想多而已。

真是豁然开朗啊!原来这就是一个普普通通的 “Qualified name lookup”嘛!就是 C::F嘛!只不过是 C写了好多几次,变成: C::C::C::F 而已嘛!结合本例,把 C 用 Entity 代入,把 F 用Entity 的静态成员调用 foo() 代入,得到:

Entity::Entity::Entity::foo();

按照 “Qualified name lookup” 规则,:: 左边的 Entity 应优先按 类名查找,于是找到 class Entity,并且它里面还正好有 foo 成员,并且还正好是 一个静态成员,可以直接通过 类名来调用,这就是: Entity::foo()。

切慢,前面还是有一大串 Entity::Entity:: 怎么解析或解释?也好办, 既然已经 是 C::F 形式,而不是特例 “C::C”形式,于是有关 Entity::Entity 是一个构造函数名字的选项,就已经失效,此时统一走 “Qualified name lookup”, 再结合 “Injected-class-name” 规则, Entity::Entity 就是得到类名 “Entity”,于是再多层 “Entity::Entity::Entity::Entity”,两两结合后,最终得到仍然是 一个类名:“ Entity”。而 类名 + :: + 静态成员函数,比如: “Entity::foo()”,不就是一次再普通不过的静态成员函数的调用吗?

夜色已深,古老的C++部落再次恢复它的安宁。


各位不能打我,其实我还是讲了很多C++方面的科学知识的。