C++如何解析函数调用
C语言是一个简单的语言。用户针对每一个函数,只能设置一个唯一的函数签名。但是C++而言,就给了我们很多的灵活性:
针对C++提供的这些特性,你可以实现 但是,在我们充分利用这些特性时,往往很容易将事情推向错误的一面。在某些情况下,编译器在接受我们的代码时,会给出如下的报错:
和很多C++程序员一样,我也经常苦恼与这样的问题。每次出现这样的报错,我总是大脑一片空白,在网上查询更好的理解,然后修改代码知道程序可以运行。在最近的一些项目开发中,我再次被这样的问题阻扰;它变得和我认知中这类问题的理解完全对立,我因此意识到我对于这类问题的理解还不够充分,仍有缺失。 幸运的是,如今是2021年了,网络信息如此发达;在此,我尤其感谢cppreference.com,如今我知道我对于此类问题缺失的理解:一个隐藏算法的清晰全貌,用来在编译时的每一次函数调用。 这也是给定编译器,一个函数调用表达式,准确计算出哪一个函数被调用: 上图这些步骤隐藏在C++标准的背后,每一个C++编译器都要遵循这些规则,并且这一系列的函数调用所涉及的程序表达式计算,都发生在编译时。这也是C++能够支持上面种种特性的原因。 我个人猜想,上图整个算法的意图就是----实施程序员所希望的操作,并且在某种程度上,它是成功的。作为程序员,在大部分时间和开发场景上,是完全可以忽略这些背后的算法;但是,如果涉及开发一个库,你最好了解这些规则。 所以,让我们从入门到放弃(开玩笑)的了解这些背后的算法机制,对于很多有经验的C++程序员,本文聊到的内容都是相当熟悉的东西。此外,我也希望抛砖引玉,给大家带来一些新颖的C++子话题,例如:参数独立查询和SFINAE,但是我们不会特别的深入探讨这些字话题。因此,本文的定位,是给大家带来C++函数调用在编译时的一些列操作策略。 命名查询我们的旅途始于一个函数调用表达式,例如,采用这个表达式
回答这个问题的第一步叫做:命名查询。在这一步,编译器在编译当下此时此刻,查询出所有具有所给定查询名称的函数、函数模版和其他可被引用的标识符,如下图。 如上述流图所示,有三个主要被查询的名称类型,每一个都有各自的规则。
我们的例子中,给出一个没有修饰的名称。那么,当运算一个函数调用表达式,从而查询一个名称操作时,编译器就可能找到多个声明,我们把这些多个声明都叫做候选。上述事例中,编译器可以找到三个候选: 上图中,圈出的第一个候选需要额外关注,因为它表明一个简单的C++特性,也就是:参数依赖查询----ADL。正常情况下,你不希望这个函数作为候选,因为它是声明在 原因就是, 任何时刻,当你使用一个没有修饰符的名称在一个函数调用过程中,并且这个名称不是引用一个类成员,此时ADL引入,可以更加广泛的查询符合的候选。特别的,在一般使用情况下,编译器会在参数类型的命名空间中查询合适的候选函数,也就符合“参数依赖查询”的意思。 完整的ADL规则,有着更加详细的差异描述,但是,可以确定的是,ADL只适用于无修饰的名称。对于有修饰的名称,也就是在单个范围内查询,那么使用ADL规则是没有意义的。ADL同样适用于重载内置操作符,例如: 函数模版的特殊具柄通过名称查询的一些候选是函数,另外则是函数模版。对于函数模版存在一个问题:我们无法调用它们,我们只能调用函数。因此,在名称查询后,编译器遍历每一个候选,并试图将每一个函数模版转为函数。 上面,我们给出的示例中,存在一个候选就是函数模版:
这个函数模版仅有一个模版参数 上图中,编译器将 在候选列表中的所有函数模版生命周期到这一步结束:模版参数替换。在这一步,编译器接受函数模版声明,并用对应的模版参数替换掉每个模版参数出现的地方。在我们的例子中,模版参数 当然,存在一些情况下,模版参数替换失败。假设下面的情况,相同的函数模版接受一个第三个参数:
那么,编译器会使用 当模版参数替换失败,那么函数模版就被移除出候选列表;在C++历史中的某些时刻,人们意识到这样的特性是可以挖掘利用的,这样的发现导致了整个模版元编程技术的出现,常被称作SFINAE(substitution failure is not an error)。 重载解析在这个阶段,通过名称查询的所有函数模版都已经消失,我们获得了一组干净、漂亮的候选函数,它们也同样被称为重载组。下面就是我们例子中的重载组: 接下来两步就是缩小这个集合,通过决定哪个候选函数可以保留,也就是说,哪一个候选函数可以处理函数调用。 或许,最显而易见的要求就是,参数必须匹配;这也就是说,一个保留函数,应该能够接受调用者的参数。如果,调用者的参数类型和实际的函数参数类型不匹配,它至少是可以实现隐式转换每个参数到与之要对应的参数类型。让我们看看给出的例子中的候选函数,是否有与之匹配的参数: 候选1调用者第一个参数类型是 候选2调用者第一个参数类型是 候选3候选3的参数类型等同于候选1,所以,它也是匹配的。 最后的决策在这一步,我们的例子只剩下最后两个保留的函数: 实际上,如果上图两个候选中,任意一个被保留下来,那么它就是最后执行函数调用的具柄。但是由于最后还有两个候选,编译器必须在多个候选中进一步进行操作:它必须决定哪一个是更好的候选函数。为了成为最好的候选函数,它们当中必须有一个更加的匹配,这就是由决胜者规则序列决定的: 下面给出三条决胜者规则: 首要决胜者:参数最匹配者胜出C++强调了调用者参数类型和函数参数类型匹配程度的重要性,宽泛来说,编译器倾向选择函数需要较少隐式类型转换的的候选函数。这条规则决定了我们在使用 在我们的例子中,由于两个候选函数的参数类型都一致,所以,第一条规则都满足。 第二决胜者:非模版参数胜出如果第一条规则没有决出胜负,那么C++倾向于调用非模版函数。在我们的例子中,由于候选1是非模版函数,而候选2是模版函数,因此,我们的最优函数就是:
值得重申的是,如果有一个模版函数在参数类型上更加匹配,那么该模版函数胜出,也就是说,决胜者规则的优先级是按顺序递降的。 第三决胜者:更加特定的模版胜出我们的例子中,最优的候选函数已经获得,但是如果没有得的话,那么我们就要参考第三决胜者规则。这条规则中,C++倾向于调用“更加特定”的模版函数,例如,考虑下面两个函数模版:
在进行模版参数推理步骤时,第一个函数模版接受任意类型作为它的第一个参数,而第二个函数模版仅仅接受指针类型。因此,第二个函数模版被称为更加特定。因此,编译器倾向于选择第二个候选函数作为最优函数。 函数调用解析之后在此时,编译器已经准确知道了哪一个函数应该作为表达式
(编辑:北几岛) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |