提问人:Chilippso 提问时间:7/15/2022 最后编辑:Chilippso 更新时间:7/21/2022 访问量:229
重载分辨率和数组到指针衰减 - 为什么 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
问:
TL的;博士;为什么 - 关于重载分辨率/函数声明 - 被视为比 更精确的匹配,即使传递的变量确实是类型而不是“只是”的pointer type
array type
array type
pointer type
我已经查看了一些关于重载解析和数组到指针衰减的等效问题(例如 21972652、29602638 和 72929287),并了解到编译器在传入时会处理 和 ,但我真的不明白它为什么这样做(尽管规范中说明要这样做)。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* a
int[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)
答:
那么为什么编译器不赞成 void 而不是 void 呢?
foo(int (&a)[2])
foo(int* a)
因为数组到指针的转换并不被认为比标识转换差。这可以从 ics.rank-3.2.1 中看出,它指出:
如果满足以下条件,则标准转换序列 S1 是比标准转换序列 S2 更好的转换序列
- S1 是 S2 的正确子序列(比较由 [over.ics.scs] 定义的规范形式的转换序列,不包括任何左值转换;恒等转换序列被视为任何非恒等转换序列的子序列),或者,如果不是,
(强调我的)
这里需要注意的重要一点是,数组到指针的转换是左值转换。
评论
the identity conversion sequence is considered to be a subsequence of **any** non-identity conversion sequence
excluding any Lvalue Transformation
@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 所需的转换序列
让我们首先确定这两个调用需要哪些转换序列:
void foo(int (&a)[2]);
只需要身份转换序列,根据 12.4.3.1.4 引用绑定 [over.ics.ref] (1):(1)当引用类型的参数直接绑定到参数表达式时,隐式转换序列就是身份转换[...]
void foo(int* a);
需要由 Array-to-pointer 转换转换组成的转换序列,根据 7.3.2 Array-to-pointer conversion [conv.array] (1):(1) “N T 数组”或“T 的未知边界数组”类型的左值或右值可以转换为“指向 T 的指针”类型的值。[...]
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:任何一个转换序列都不是另一个转换序列的子序列。
为了充分理解这条规则,我们需要首先定义几个术语:
- 序列(如标准转换序列)是指数学序列;从本质上讲,数学中的序列是一个具有附加约束的集合,即元素的顺序很重要,并且允许元素的重复。
- 适当的子序列(或严格的子序列)
给定序列 和 ,如果 是 的子序列,并且不等于 ,则 是 的正确子序列。() (参见子集/适当的子集 - 序列也是如此)
下面举几个例子来说明这个原理:A
B
A
B
A
B
A
B
(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 样式数组的大多数常见陷阱。(数组有什么问题?)
评论
,
不包括任何左值转换;恒等转换序列被认为是任何
非恒等转换序列的子序列
- 因为 我是沉思的over.ics.rank#3.1.2
;
评论
int (&a)[N]
int* a
std::array
void foo(int&)
std::vector
std::span
std::size_t length