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 有一个基类叫 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++方面的科学知识的。