C++中,C::C::C::C::foo() 为什么编译成功?

这篇具有很好参考价值的文章主要介绍了C++中,C::C::C::C::foo() 为什么编译成功?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

有人问:

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++ 中的 以下 知识点:

  1. “Unqualified name lookup / 未限定的名字查找”
  2. “Qualified name lookup / 有限定的名字查找”
  3. “Injected-class-name/ ‘注入式’类名称 ”以及 特别关注其中的 “Injected-class-name and constructors / ‘注入式’类名称 和 构造函数 的关系 ”。
  4. “ Elaborated type specifiers / 详细的类型说明符 ”
如果被限定的名称,最终发现是一个C++基础类型 (bool, int, char 等),那还得牵涉出: "pseudo-constructor-name 或 pseudo-destructor-name / 伪构造或伪析构名字 ” 等等

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

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

struct A {};

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

问: using 这行代码能编译通过吗?

答:不行的,编译将得到类似 “A::A 是构造函数,不是类型”这样的出错信息。如下图:

C++中,C::C::C::C::foo() 为什么编译成功?,白话C++,C++面试初级“卷”,开发语言,c++

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

struct B {};
struct A : B {};

using T = A::B; // 编译成功

int main() {}

那要怎么让 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::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 。

对于“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”,这回成功了。


现在来看问题中的 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++方面的科学知识的。文章来源地址https://www.toymoban.com/news/detail-530875.html

到了这里,关于C++中,C::C::C::C::foo() 为什么编译成功?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 家里用的无线路由器连接成功后,为什么还不能上网?

    如今,家庭用无线路由器非常普遍,不但在家庭中使用,企事业单位更是频繁。再加上便携式mini型无线路由器的推陈出新,无线路由器可谓随处可见。但很多人应该都会碰到家里用的无线路由器连接成功后,不能上网的情况吧,不用担心,小编针对几种情况详细为大家介绍讲

    2024年02月06日
    浏览(57)
  • 为什么开发者关系对 Web3 的成功至关重要?

    之前,我们研究了早期初创公司如何建立开发者社区和跟踪产品与市场的契合度。如果没有有效的开发者关系团队,这些举措不太可能成功。 开发者关系专业人员通常作为信息枢纽,经常与产品、销售和营销等其他运营团队合作,并让每个人都了解情况。由于许多Web3初创公

    2023年04月08日
    浏览(57)
  • 【2023,学点儿新Java-14】携程面试题:如何看待Java是一门半编译半解释型的语言?| 咨询互联网行业 资深前辈的一些问题 | 附:为什么说ChatGPT的核心算法是...?| GPT-3.5

    前情回顾: 【2023,学点儿新Java-13】阶段练习之Java面试企业真题(阿里巴巴拼多多 等) | 常用的Java命令行操作都有哪些 | 如何解决Java的内存泄漏和内存溢出问题? 【2023,学点儿新Java-12】小结:阶段性复习 | Java学习书籍推荐(小白该读哪类Java书籍?有一定基础后,再去读

    2024年02月09日
    浏览(48)
  • 面试题:为什么要合并 HTTP 请求?有什么好处?

    为什么要实现batch call? - 减少网络中的传输损耗 - 如何减少的? - 通过合并HTTP请求 - 合并HTTP请求是如何减少网络损耗的? 本文将解决这个问题。一起看看单个请求携载大量信息和多个请求携载小量信息对于整个时间的影响。 可以保持长连接,但是每个不同的请求之间,clien

    2024年01月19日
    浏览(47)
  • 面试题:Kafka 为什么那么快?

    有人说:他曾在一台配置较好的机子上对 Kafka 进行性能压测,压测结果是 Kafka 单个节点的极限处理能力接近每秒 2000万 条消息,吞吐量达到每秒 600MB。 那 Kafka 为什么这么快?如何做到这个高的性能? 本篇文章主要从这 3 个角度来分析: 生产端 服务端 Broker 消费端 先来看下

    2024年01月22日
    浏览(54)
  • Redis为什么快?(面试常问)

    Redis 是一个开源的高性能内存数据库,特点是数据存储在内存中,操作时性能更高;还支持多种数据结构,String、Hash、list、set、zset等,key还支持自动过期。 Redis的好处 是因为数据存在内存中所以性能更高,还有因为是单线程操作,所以天然具有线程安全的特性,单线程又能

    2024年02月11日
    浏览(41)
  • 面试官灵魂拷问:什么是MySQL索引?为什么需要索引?

    关系型数据库是一种采用关系模型存储数据的数据库系统。在关系型数据库中,数据被组织成一个或多个表格(也称为关系),每个表格包含多行记录,每行记录代表一组相关数据。每个表格都有一个定义该表格中数据的结构的模式,即表格的列和每个列的数据类型。关系型

    2024年02月09日
    浏览(62)
  • Mybatis为什么需要预编译等一系列问题

    SQL 预编译是一种提高数据库访问效率的技术,它通过将 SQL 语句预编译并存储在数据库中,减少每次执行时需要进行解析和编译的开销,从而提高数据库访问的效率。 在预编译阶段,SQL 语句会被解析并转换为可执行的二进制代码,然后存储在数据库中。当需要执行该 SQL 语句

    2024年02月10日
    浏览(43)
  • 大数据面试题:HBase为什么查询快

    面试题来源: 《大数据面试题 V4.0》 大数据面试题V3.0,523道题,679页,46w字 可回答:1)HBase为什么读快;2)HBase是根据rowkey查询,当数据量相当大的时候,是怎么读的很快的 参考答案: 1、基于LSM树的存储方式 HBase采用基于LSM树的存储方式,这种存储方式将数据分为内存和

    2024年02月12日
    浏览(45)
  • 面试官问:kafka为什么如此之快?

    天下武功,唯快不破。同样的,kafka在消息队列领域,也是非常快的,这里的块指的是kafka在单位时间搬运的数据量大小,也就是吞吐量,下图是搬运网上的一个性能测试结果,在同步发送场景下,单机Kafka的吞吐量高达17.3w/s,不愧是高吞吐量消息中间件的行业老大。 那究竟

    2024年02月07日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包