C ++如何解析函数调用

2021-03-16 10:44:15

c是一种简单的语言。您只允许使用每个名称的函数。另一方面,C ++为您提供更大的灵活性:

我喜欢这些c ++功能。使用这些功能,您可以使str1 + str2返回两个字符串的串联。您可以有一对2D点,另一对3D点,并过载点(A,B)以与任一类型一起使用。您可以拥有一堆类似数组类的类,并编写一个与所有的单个排序函数模板合作。

但是当你利用这些功能时,很容易推动太远。在某些时候,编译器可能会意外地拒绝具有错误的错误,如:

错误C2666:' string ::运算符==&#39 ;: 2 overloads有类似的转化 注意:可以是' bool string ::运算符==(const String&)const' 注意:或'内置C ++运算符==(const char *,const char *)' 注意:在尝试匹配参数列表时'(const字符串,const字符*)'

像许多C ++程序员一样,我在整个职业生涯中挣扎着如此错误。每次发生,我通常会划伤我的头部,在线搜索更好的理解,然后更改代码直到它编译。但最近,在为胶合板开发一个新的运行时库时,我一遍又一遍地挫败了这类错误。很明显,尽管我以前的所有与C ++的经历,但我的理解中遗漏了一些事情,我不知道它是什么。

幸运的是,现在是2021年,关于C ++的信息比以往更全面。特别感谢CpPreference.com,我现在知道我的理解中缺少了什么:清晰的隐藏算法的图片在编译时为每个函数调用运行。

这就是编译器,给定函数调用表达式的方式,符合哪个函数来调用:

可以执行参数依赖查询可能涉及Sfinae非模板函数函数模板发现隐式转换候选功能可行函数Receethers模板参数演示参数参数替换参数必须满足(C ++ 20)更好匹配的参数赢得胜利模板函数获取更多专业化模板赢得额外的纠结者否则过载解析名称查找功能模板的特殊处理除名称查找合格名称查找Mement Member Name查找这些步骤在C ++标准中枚举。每个C ++编译器必须遵循它们,并且整个事情会在编译时发生在程序中的每个函数调用表达式。在后威尔,很明显必须是这样的算法。这是C ++可以同时支持所有上述功能的唯一方法。当您将这些功能组合在一起时,这就是你得到的。

我想到了算法的总体意图是“做程序员期望的事情”,并且在某种程度上,它是成功的。您可以非常远得完全忽略算法。但是,当您开始使用多个C ++功能时,就像在开发库时,最好了解该规则。

所以让我们从头到尾走过算法。我们覆盖的许多人都会熟悉C ++程序员。尽管如此,我认为它可以是非常令人关注的是,看看所有步骤如何合适。 (至少是对我而言。)我们将沿着几个先进的C ++副主题触摸几个先进的C ++副主点,如参数依赖的查找和Sfinae,但我们不会深入潜入任何特定的疑比奇。这样,即使您对副主题读不过任何其他信息,您将至少知道它是如何符合C ++在编译时解决函数调用的总体策略。我认为这是最重要的事情。

我们的旅程从函数调用表达式开始。例如,在下面的代码列表中表达BLAST(AST,100)。此表达式清楚地意味着调用名为BLAST的函数。但哪一个?

命名空间Galaxy { struct小行星{ 浮动半径= 12; }; 无效爆炸(小行星* AST,浮动力); } 结构目标{ Galaxy ::小行星* AST; 目标(Galaxy :: Serteroid * AST):AST {AST} {} 操作员Galaxy ::小行星*()const {返回AST; } }; BOOL BLAST(目标目标); 模板< typename t> void blast(t * obj,浮动力); void play(galaxy :: serteroid * ast){ 爆炸(AST,100); }

回答这个问题的第一步是名称查找。在此步骤中,编译器看起来已被声明到此点的所有函数和函数模板,并标识可由给定名称引用的函数和函数模板。

可以执行参数依赖查找不合格的名称查找合格名称查找成员名称查找作为流程图所示,有三种主要类型的名称查找,每个类型都有自己的一组规则。

当名称到右侧时,会发生成员名称查找。或 - >令牌,如foo->酒吧。此类查找用于查找类别成员。

当名称中有一个::令牌时,会发生合格的名称查找,如std :: sort。这种类型的名称是显式的。 ::令牌右侧的部分仅在左侧部分标识的范围内查询。

不合格的名称查找既不是那些。当编译器看到不合格的名称,如BLAST,它会根据上下文查找各种范围中的匹配声明。有一套详细的规则,确定编译器应该看的位置。

在我们的案例中,我们有一个不合格的名称。现在,当对函数调用表达式执行名称查找时,编译器可能会找到多个声明。让我们致电这些宣言候选人。在上面的示例中,编译器查找三个候选人:

void galaxy :: blast(Galaxy ::小行星* AST,浮动力)BOOL BLAST(目标目标)模板< typename t> void blast(t * obj,浮动力)这一个来自Galaxy命名空间2 1 3上面旋转的第一个候选者,值得额外关注,因为它展示了易于忽略的C ++的特征:参数依赖的查找,或者ADL短的。我承认,我花了大多数我的C ++职业生涯没有意识到ADL在名称查找中的角色。这是一个快速摘要,以防你在同一条船上。通常情况下,您不会指望这种功能成为此特定呼叫的候选者,因为它在Galaxy命名空间内宣布,并且呼叫来自Galaxy命名空间之外。在代码中没有使用命名空间Galaxy指令来使此功能可见。那么为什么这职能是候选人?

原因是因为您在函数调用中使用不合格的名称的原因 - 并且名称没有引用类成员,以及其他事情 - ADL启动,名称查找变得更加贪婪。具体而言,除了通常的地方之外,编译器还查找参数类型的命名空间中的候选函数 - 因此名称“参数依赖查找”。

BLAST(AST,100)函数调用表达式不合格名称参数类型是Galaxy ::小行星* Galaxy命名空间被驱逐用于候选函数(参数依赖查询)管理ADL的完整规则集比我在此描述的内容更细致,但关键是ADL仅适用于不合格的名称。对于合格的名称,在一个范围内被抬头,没有点。 ADL还在超载内置运算符,如+和==,这让您在写作时享受它,例如Math库。

有趣的是,有些案例会员名称查找可以找到不合格名称查找的候选人。 Eli Bendersky看到这篇文章有关这一点。

名称查找的一些候选人是功能;其他是功能模板。功能模板只有一个问题:您无法调用它们。您只能调用函数。因此,在名称查找之后,编译器通过候选者列表,并尝试将每个函数模板转换为函数。

可能涉及Sfinae非模板函数函数模板模板参数参数扣除模板替换在我们一方面的示例中,其中一个候选人确实是一个函数模板:

模板< typename t> void blast(t * obj,浮动力)3这个函数模板具有单个模板参数t。因此,它需要一个单一的模板参数。来电者,BLAST(AST,100)没有指定任何模板参数,所以为了将此功能模板转换为函数,编译器必须弄清楚T.这就是模板参数扣除的位置。在此步骤,编译器将调用者(下图中的左侧)的函数参数的类型进行比较到函数模板(在右侧)预期的函数参数的类型。如果右侧引用任何未指定的模板参数,则编译器尝试使用左侧的信息推断它们。

模板< typename t> void blast(t * obj,浮动力)来电(AST,100)函数模板参数参数参数AST 100 Galaxy ::小行星* int T * float T被推导为Galaxy ::小行星在这种情况下,编译器推断T作为Galaxy ::小行星,因为这样做使得第一个功能参数T *与Argument AST兼容。管理模板参数扣除的规则是一个重要的主题,而是在这样的简单示例中,他们通常会做你所期望的。如果模板参数扣除不起作用 - 换句话说,如果编译器无法以使功能参数与呼叫者参数兼容的方式 - 那么函数模板,则从候选列表中删除。

在此时生存到这一点的候选人列表中的任何功能模板都会受到下一步:模板参数替换。在此步骤中,编译器采用函数模板声明,并用其对应的模板参数替换每个模板参数的每次发生。在我们的示例中,模板参数T被推导的模板参数Galaxy ::小行星替换。当此步骤成功时,我们终于拥有可以调用的真实功能的签名 - 不仅仅是一个函数模板!

模板< typename t>无效爆炸(t * obj,浮动力)无效< galaxy ::小行星>(Galaxy ::小行星* Obj,浮动力)替换当然,存在模板参数替换可能失败的情况。假设相同函数模板接受了第三个参数的那一刻,如下所示:

如果是这种情况,编译器将尝试用Galaxy ::小行星替换T :: Units中的T。由此产生的类型说明符,Galaxy ::小行星::单位,因为Surrul Galaxy ::小行星实际上没有成员名为单位的成员。因此,模板参数替换将失败。

当模板参数替换失败时,函数模板只是从候选人列表中删除 - 并且在C ++历史的某些时候,人们意识到这是一个他们可以利用的功能!发现在自己的权利中导致了一整套成分数程技术,统称为Sfinae(替换失败不是错误)。 Sfinae是一个复杂的,笨重的主题,我只是说这两件事。首先,它基本上是钻机函数调用解决过程中选择所需的候选方式。其次,随着程序员越来越多地转向现代C ++成分的技术,它可能会随着时间的推移而偏离达到相同的事情,如限制和Constexpr,如果。

在此阶段,名称查找期间发现的所有功能模板都消失了,我们留下了一个很好的整个候选函数。这也被称为过载集。以下是我们示例的更新候选函数列表:

void galaxy :: blast(Galaxy ::小行星* AST,浮动力)Bool Blast(目标目标)空隙爆炸< Galaxy ::小行星>(Galaxy ::小行星* Obj,浮动力)2 1 3下两个步骤缩小甚至进一步通过确定哪个候选功能是可行的,换句话说,换句话说,哪些可以处理函数调用。

发现隐式转换候选函数可行函数参数必须是兼容的约束必须满足(C ++ 20)可能是最明显的要求是参数必须兼容;也就是说,活函数应该能够接受来电者的参数。如果来电者的参数类型完全不匹配函数的参数类型,则至少可以将每个参数隐式转换为其相应的参数类型。让我们来看看我们的每个示例的候选函数,看它的参数是否兼容:

来电者的论证类型候选星系::小行星*浮动目标Galaxy ::小行星*浮动Galaxy ::小行星* int函数参数类型2 1 3候选人候选人来电者的第一个参数类型Galaxy ::小行星*是一个完全匹配的匹配。调用者的第二个参数类型int隐式可将其敞开的第二个函数参数类型float,因为int浮动是标准转换。因此,候选1的参数兼容。

调用者的第一个参数类型Galaxy ::小行星*被隐式兑换为第一个函数参数类型目标,因为目标具有接受Galaxy ::小行星类型的参数的转换构造函数。 (顺便提及,这些类型也在另一个方向上敞开,因为目标具有返回Galaxy ::小行星*的用户定义的转换函数。但是,呼叫者通过了两个参数,候选2只接受一个参数。因此,候选2不可行。

void galaxy ::爆炸(Galaxy ::小行星* AST,浮动力)Bool Blast(目标目标)无效< Galaxy ::小行星≫(Galaxy ::小行星* Obj,浮动力)2 1 3

候选3的参数类型与候选1相同,因此它也兼容。

与此过程中的所有其他内容一样,控制隐式转换的规则是他们自己的整个主题。最值得注意的规则是您可以避免让构造函数和转换运营商通过明确标记它们来参与隐式转换。

使用呼叫者的参数过滤掉不兼容的候选物后,编译器继续检查每个功能的约束是否满足,如果有任何情况。约束是C ++ 20中的一个新功能。他们让您使用自定义逻辑来消除候选函数(来自类模板或功能模板),而无需诉诸Sfinae。他们也应该为您提供更好的错误消息。我们的示例不使用约束,因此我们可以跳过此步骤。 (从技术上讲,标准说,在模板参数扣除期间还要先前检查约束,但我跳过该细节。检查两个地方有助于确保显示最佳可能的错误消息。)

此时在我们的示例中,我们将达到两个可行的功能。他们中的任何一个都可以处理原始函数调用很好:

void galaxy :: blast(Galaxy ::小行星* AST,浮动力)无效爆炸< Galaxy ::小行星::小行星>(Galaxy ::小行星* Obj,浮动力)2 1确实,如果上述任一职能是唯一可行的,它将处理函数调用的那个。但是因为有两个,编译器现在必须在有多种可行功能时始终做到这一点:它必须确定哪一个是哪一个是最佳的可行功能。作为最好的可行函数,其中一个必须以一系列决定规则决定“赢得”所有其他可行的职能。

Witebreakers更好匹配的参数赢得非模板功能获胜更专业的模板赢得额外的纠结者,否则其他职能C ++最重要的是调用者的参数类型匹配函数的参数类型最重要。松散地说,它喜欢从给定参数中需要较少的隐式转换的函数。当两个函数都需要转换时,一些转换被视为“更好”比其他转换。例如,这决定了是否调用std :: Vector的运算符[]的const或non-const版本的规则。

在我们一方面的示例中,两个可行的函数具有相同的参数类型,因此也不比另一个更好。这是一个平局。因此,我们继续前进到第二个纠纷。

如果第一个绑定程序未解决,则C ++更喜欢通过模板函数调用非模板函数。这是决定我们的榜样的规则;可行功能1是非模板功能,而可行功能2来自模板。因此,我们最好的可行功能是来自Galaxy命名空间的人:

void galaxy ::爆炸(Galaxy ::小行星* Ast,浮动力)获胜者!值得重申,以前的两名决手将按照我描述的方式订购。换句话说,如果有一个可行的函数,其参数与给定的参数比所有其他可行的函数更好地匹配,即使它是模板函数也会获胜。

在我们的示例中,已经找到了最佳的可行功能,但如果没有,我们必须继续前进到第三个纠纷。在此纠纷中,C ++更喜欢将“更专业化”的模板功能置于“较少专业化”中。例如,考虑以下两个功能模板:

模板< typename t> void blast(t obj,浮动力); 模板< typename t> void blast(t * obj,浮动力);

当对这两个函数模板执行模板参数扣制时,第一个函数模板将接受任何类型作为其第一个参数,但第二个函数模板只接受指针类型。因此,据说第二个功能模板更为专业化。如果这两个功能模板是我们呼叫爆炸(AST,100)的名称查找的唯一结果,并且都导致了可行的功能,目前的界限规则将导致第二个挑选第一个。决定哪个函数模板的规则比另一个函数模板更加专业化是另一个重要的主题。

尽管它被认为更专业化,但重要的是要理解第二个功能模板实际上并不是第一功能模板的部分专业化。相反,它们是两个完全独立的功能模板,恰好分享相同的名称。换句话说,它们超载了。 C ++不允许部分专业化功能模板。

除了这里列出的人之外还有几个不破坏者。 例如,如果宇宙灰度< = gt; 操作员和超载的比较运算符如> 是可行的,C ++更喜欢比较运算符。 如果候选者是用户定义的转换函数,则还有其他规则可以比我所示的那些更优先级。 尽管如此,我相信我展示的三个不破坏者是最重要的。 毋庸置疑,如果编译器检查每个纠结者并没有 ......