重载分辨率和数组到指针衰减 - 为什么 int (&a)[2] 和 int* 在重载分辨率方面被认为同样精确

Overload resolution and array-to-pointer decay - why is int (&a)[2] and int* a considered equally exact regarding overload resolution

提问人:Chilippso 提问时间:7/15/2022 最后编辑:Chilippso 更新时间:7/21/2022 访问量:229

问:

TL的;博士;为什么 - 关于重载分辨率/函数声明 - 被视为比 更精确的匹配,即使传递的变量确实是类型而不是“只是”的pointer typearray typearray typepointer type


我已经查看了一些关于重载解析和数组到指针衰减的等效问题(例如 219726522960263872929287),并了解到编译器在传入时会处理 和 ,但我真的不明白它为什么这样做(尽管规范中说明要这样做)。void foo(int (&a)[3]);void foo(int* a);int bar[3];

考虑以下示例,该示例由于不明确的函数调用(在线)而无法编译:

#include <iostream>
#include <utility>

void foo(int (&a)[2])
{
    std::cout << "Called concrete foo with N = 2";
}

void foo(int* a)
{
    std::cout << "Called pointer foo" << std::endl;
}

int main()
{
    int a1[] = { 0x00 };
    int a2[] = { 0x00, 0x01 };
    int a3[] = { 0x00, 0x01, 0x02 };

    foo(a1);
    foo(a2);
    foo(a3);
}

我幼稚的理解告诉我,对于函数调用,在重载分辨率方面应该比 更具体/更精确,因为变量 a2 的实际类型是(数组类型)而不是(指针类型)。foo(a2)int (&a)[2]int* aint[2]int*

那么为什么编译器不赞成呢?void foo(int (&a)[2])void foo(int* a)

亲切问候


问题的第一部分(为什么编译器不赞成)已经回答并标记为已接受,因为这是我最初的问题,但是如果有人可以解释这个决定背后的一些原因,那就太好了,即为什么会这样,以及是否有任何反例(除了向后兼容性)可以支持。void foo(int (&a)[2])void foo(int* a)void foo(int (&a)[2])void foo(int* a)

C++ 数组 指针 参数传递 重载解析

评论

0赞 user12002570 7/15/2022
复制:模板与类似的非模板函数
0赞 Chilippso 7/15/2022
这根本不是重复的 - 我不是在问常规函数是否比函数模板更受欢迎。请特别看我的第二个示例代码。它是关于:为什么在过载分辨率方面和被认为同样准确?int (&a)[N]int* a
0赞 463035818_is_not_an_ai 7/15/2022
该标准的引用与为什么调用模棱两可没有太大关系。函数的类型不同,如果它们属于同一类型,则定义两者将导致编译器错误。这更像是一个过载分辨率的问题
0赞 user12002570 7/16/2022
另一个骗局:数组引用绑定与使用模板的数组到指针的转换
0赞 Goswin von Brederlow 7/16/2022
所有这些都可以通过使用不会衰减的 和 来避免。 也来我的。但是,如果你真的需要传递一个 C 样式的数组,你应该总是用 .std::arrayvoid foo(int&)std::vectorstd::spanstd::size_t length

答:

1赞 user12002570 7/16/2022 #1

那么为什么编译器不赞成 void 而不是 void 呢?foo(int (&a)[2])foo(int* a)

因为数组到指针的转换并不被认为标识转换差。这可以从 ics.rank-3.2.1 中看出,它指出:

如果满足以下条件,则标准转换序列 S1 是比标准转换序列 S2 更好的转换序列

  • S1 是 S2 的正确子序列(比较由 [over.ics.scs] 定义的规范形式的转换序列,不包括任何左值转换;恒等转换序列被视为任何非恒等转换序列的子序列),或者,如果不是,

(强调我的)

这里需要注意的重要一点是,数组到指针的转换是左值转换

评论

0赞 Chilippso 7/17/2022
也许我只是无法正确提问,但感谢您最终解决了我的问题。关于我的问题,这是一个有趣的“点”。尽管你强调了摘录,但它也指出.分号使我想,前面的陈述是否也适用于前面引用的陈述。然而,即使答案趋向于正确的方向,它也不如另一个完整,所以它(目前)不是公认的答案the identity conversion sequence is considered to be a subsequence of **any** non-identity conversion sequenceexcluding any Lvalue Transformation
3赞 Turtlefight 7/16/2022 #2

@463035818_is_not_a_number在他们的回答中已经解释了为什么匹配比 更好。void foo(int* a);template<std::size_t N> void foo(int (&a)[N])

此答案仅适用于问题中的第二个示例:

void foo(int (&a)[2]);
void foo(int* a);

// why is this call ambiguous?
int arr[2];
foo(arr);

1. 寻找最佳匹配函数

这两个函数都可以通过名称查找找到,分别是候选函数可行函数

因此,将调用哪个函数(或者如果调用不明确)取决于它们中的任何一个是否比另一个函数更可行。

根据 12.4.3 最佳可行函数 [over.match.best] (2)

(2) 给定这些定义,如果对于所有参数 i,ICS i(F1) 不是比 ICS i(F2) 差的转换序列,则一个可行函数 F1 被定义为比另一个可行函数 F2 更好的函数,然后 [...]

(2.1) 对于某些参数 j,ICS j(F1) 是比 ICSj(F2) 更好的转换序列,[...]

因此,为了确定这两个函数中的任何一个是否更好,我们需要检查它们参数的转换顺序并进行比较。


2. 比较转换序列

2.1 所需的转换序列

让我们首先确定这两个调用需要哪些转换序列:

2.2 哪种转换顺序更好?

为了确定哪种转换顺序更好,我们需要参考12.4.3.2 Rank隐式转换序列的排名[over.ics.rank]——在本例中主要是第(3)和(4)段。

(1) 本子句定义了基于关系的隐式转换序列的部分排序:更好的转换序列和更好的转换如果这些规则将隐式转换序列 S1 定义为比 S2 更好的转换序列,则 S2 也是比 S1 更差的转换序列。如果转换序列 S1 既不优于也不比转换序列 S2 差,则称 S1 和 S2 是无法区分的转换序列

2.3 其中一个转换序列是另一个转换序列的子序列吗?

注意:我们将跳过 (3.1),因为它仅适用于列表初始化。

因此,让我们从第一个条件(3.2.1)开始:

(3.2) 如果满足以下条件,则标准转换序列 S1 是比标准转换序列 S2 更好的转换序列:

  • (3.2.1) S1 是 S2 的适当子序列(比较由 [over.ics.scs] 定义的规范形式的转换序列,不包括任何左值转换;身份转换序列被视为任何非身份转换序列的子序列),或者,如果不是,[...]

注意:由于评论中对此条款有疑问,我将详细解释此规则。

TL的;dr:任何一个转换序列都不是另一个转换序列的子序列。

为了充分理解这条规则,我们需要首先定义几个术语:

  • 序列(如标准转换序列)是指数学序列;从本质上讲,数学中的序列是一个具有附加约束的集合,即元素的顺序很重要,并且允许元素的重复。
  • 适当的子序列(或严格的子序列
    给定序列 和 ,如果 是 的子序列,并且不等于 ,则 是 的正确子序列。() (参见子集/适当的子集 - 序列也是如此)
    下面举几个例子来说明这个原理:
    ABABABAB
    • (A, B)是 (我们可以删除元素) 的正确子序列(A, B, C, D)
    • (B, C)是 的正确子序列(允许在任何位置删除)(A, B, C, D)
    • (A, B, C)不是 (order matters) 的正确子序列(A, C, B, D)
    • (A)不是 的正确子序列(如果两个序列相等,则另一个序列都不是正确的子序列)(A)
    • ()是 的正确子序列(空序列是所有非空序列的正确子序列)(A, B, C)
  • 恒等式(如恒等式转换/恒等式转换序列)是指数学恒等式元素(通常简称为独立元)
    • 在本例中,术语身份转换和身份转换序列是指空的转换序列() - 不对值应用任何转换总是会导致原始值:()value ∘ () = value

因此,有了这个,我们可以分解规则:(3.2.1)

  • 比较由 [over.ics.scs] 定义的规范形式的转换序列,不包括任何左值转换

    • [over.ics.scs] 描述了我们需要对转换进行排序的方式(如果我们有多个转换) - 因为请记住排序对序列很重要
    • 如果转换序列包含左值转换,则需要在检查正确的子序列之前将其删除。
  • 身份转换序列被视为任何非身份转换序列的子序列

    • 这是一种迂回的说法,即空转换序列被视为任何非空转换序列的子序列(正确子序列的规则之一)。
  • 如果满足以下条件,标准转换序列 S1 是优于标准转换序列 S2 的转换序列:

    • S1 是 S2 的正确子序列
    • 因此,我们需要检查 S1 是否是 S2 的正确子序列,如果是,则 S1 优于 S2

因此,现在让我们将其应用于我们的特定情况:

  • 我们有序列 S1 : (身份转换序列)foo(int (&a)[2])()
  • 和序列 S2 为:foo(int* a);("Array-to-pointer conversion")

现在我们需要删除左值转换。 在 12.4.3.1.1 标准转换序列 [over.icss.scs] (3) 数组到指针的转换被列为左值转换,因此我们必须将其从 S2 中删除。

所以 S2 =()

现在我们需要检查 S1 是否是 S2 的正确子序列。

它不能是正确的子序列,因为 S1 = S2,因此此规则不适用。

2.4 对转换序列进行排名

接下来,我们需要根据 (3.2.2) 检查转换序列的排名

(3.2.2) S1 的等级优于 S2 的等级,或者 S1 和 S2 的等级相同,可以通过下文段落中的规则来区分,或者,如果不是这样,

根据 12.4.3.1.1 标准转换序列 [over.ics.scs] (3),转换有三种可能的等级:完全匹配、升级或转化:

(3) 每次转化 [...] 还具有相关的等级(完全匹配、促销或转化)。这些用于对标准转换序列进行排序。转换序列的等级是通过考虑序列中每个转换的等级和任何引用绑定的等级来确定的。如果其中任何一个具有转化等级,则该序列具有转化等级;否则,如果其中任何一个具有 Promotion 等级,则该序列具有 Promotion 等级;否则,序列具有“完全匹配”等级。

转换 类别
无需转换 身份 完全匹配
左值到右值的转换 左值变换 完全匹配
数组到指针的转换 左值变换 完全匹配
函数到指针的转换 左值变换 完全匹配
资格转换 资格调整 完全匹配
函数指针转换 资格调整 完全匹配
积分促销 晋升 晋升
浮点提升 晋升 晋升
积分转换 转换 转换
浮点转换 转换 转换
浮积分转换 转换 转换
指针转换 转换 转换
指针到成员的转换 转换 转换
布尔转换 转换 转换

其顺序如下:

12.4.3.2 对隐式转换序列进行排序 [over.ics.rank] (4)

(4) 标准转化序列按其等级排序:完全匹配是比促销更好的转化,而晋升是比转化更好的转化。除非适用以下规则之一,否则两个具有相同等级的转换序列是无法区分的:[...]

(该段中还列出了一堆例外情况,但它们都不适用于给定的例子)

所以从上表可以看出:

  • 身份转换的等级为“完全匹配”
  • 数组到指针的转换等级为“完全匹配”

因此,两个转化序列都具有“完全匹配”等级,因此我们需要检查以下规则之一 (3.2.2) 是否适用于我们的情况:

  • (3.2.3) S1 和 S2 包括引用绑定 [...]
    (3.2.4) S1 和 S2 包括引用绑定 [...]

    只有一次通过引用的绑定重载,因此这两个子句都不适用。foo

  • (3.2.5) S1 和 S2 的区别仅在于它们的资格转换 [...]

    S1 和 S2 均不包含资格转换,因此本子条款不适用。

  • (3.2.6) S1 和 S2 包括引用绑定 [...]

    不适用 - 请参阅 (3.2.3) / (3.2.4) 的推理

因此,不幸的是,没有子句可以使两个转换序列区分开来。

2.5 转换序列无法区分

[over.ics.rank] 中唯一剩下的段落是 (3.3) - 它处理用户定义的转换序列。

因此,我们已经用完了可以解决歧义的段落 - 因此,这两个函数所需的转换序列是无法区分的转换序列


3. 分辨率

由于两个 s 的转换序列是不可区分的,因此这两个函数都不是更好的可行函数。foo

因此,12.4.3 最佳可行函数[over.match.best] (3) 的第二部分适用:

如果正好有一个可行函数比所有其他可行函数更好,那么它就是通过重载解析选择的函数;否则,调用的格式不正确

因此,调用的格式不正确,将导致不明确的函数调用错误。


4. 展望未来

从 2013 年开始,就这种情况存在一个开放的核心语言问题:CWG 1789

当前的规则举了一个例子,例如

template<class T, size_t N> void foo(T (&)[N]);
template<class T> void foo(T *t);

int arr[3]{1, 2, 3};
foo(arr);

不明确,即使第一个是恒等匹配,第二个需要左值转换。这是可取的吗?

根据 2021 年提出的解决方案,建议在 12.4.3.2 中添加另一个项目符号 对隐式转换序列进行排名 [over.ics.rank] (3) 以以下形式:

S1 是数组的引用绑定,S2 是数组到指针的转换 (7.3.3 [conv.array])。
[示例 7:

 template<class T, unsigned N> void f(T (&)[N]); // #1
 template<class T> void f(T *t);                 // #2
 int r[3]{1, 2, 3};
 void g() {
   f(r);    // OK: calls #1
 }

——结束示例]

这将使转换顺序比转换顺序更好,从而解决歧义。( 总是被调用)void foo(int (&a)[2]);void foo(int* a);void foo(int (&a)[2]);

不幸的是,它仍处于审查状态,因此尚不清楚何时(甚至是否)将包含在标准中。

4.1 如何绕过C++20中的问题

有一种简单的方法可以避免歧义:制作模板(非模板化函数优先于模板化函数)。void foo(int*)

因此,这组函数不会是模棱两可的:

void foo(int (&)[2]) {}

template<class = void>
void foo(int*) {}

另一种选择是始终使用 std::array - 这也避免了 c 样式数组的大多数常见陷阱。(数组有什么问题?)

评论

1赞 Chilippso 7/17/2022
谢谢你的回答。它完美地反映了导致观察到的行为的规范“链”。尽管如此,现在我仍然很好奇“为什么”它是这样的,以及是否有任何例子来展示为什么它必须像现在这样(即除了“向后兼容性”之外,是否还有一些反例)
0赞 Turtlefight 7/19/2022
@Chilippso 我已经更新了我的答案 - 不幸的是,在重载解决的上下文中,关于对数组的引用的标准建议方面没有任何可找到的。所以不幸的是,没有一个明显的原因为什么会是这样。
0赞 Chilippso 7/19/2022
感谢您的额外努力。关于向后兼容性,我考虑的是遗留代码,而不是 C。但没关系,反正就是这样。顺便说一下,正如另一个答案的评论中所述,如果 S1 是 S2 的正确子序列,则 S1 优于 S2,比较由 [over.ics.scs] 定义的规范形式的转换序列不包括任何左值转换;恒等转换序列被认为是任何非恒等转换序列的子序列 - 因为 我是沉思的over.ics.rank#3.1.2;
0赞 Turtlefight 7/20/2022
@Chilippso,我在最新的编辑中详细阐述了整个转化序列排名,并在 [over.ics.rank] (3.1.2) 中的所有其他段落中添加了注释,详细说明了它们不适用的原因。对不起,维基百科链接和巨大的文字墙:D
1赞 Turtlefight 7/21/2022
@Chilippso,我设法找到了一个与这种歧义直接相关的核心问题(CWG 1789)——看起来标准委员会已经意识到了这种歧义,并计划解决它。不幸的是,它只处于审查阶段。我还编辑了我的答案,以包括对上述核心问题的引用。