调用重载的 <brace-enlinedinitializer list> 对于不可转换的类型是不明确的

zjy*_*qs 7 c++ language-lawyer braced-init-list

相关问题:


问题与12类似,但不一样。


#include <type_traits>
#include <vector>

struct A {
    A();
};
static_assert(std::is_convertible_v<double, A> == false);
static_assert(std::is_convertible_v<A, double> == false);

void func(std::vector<double> values);
void func(std::vector<A> as);

int main() {
    func({ 4.2 });
}
Run Code Online (Sandbox Code Playgroud)

显然,doubleA不能相互隐式转换。所以我觉得void func(std::vector<double>)应该叫。

但不同编译器的结果是不同的: https://godbolt.org/z/c1hW47f4c

GCC 无法编译:

<source>: In function 'int main()':
<source>:14:9: error: call of overloaded 'func(<brace-enclosed initializer list>)' is ambiguous
   14 |     func({ 4.2 });
      |     ~~~~^~~~~~~~~
<source>:10:6: note: candidate: 'void func(std::vector<double>)'
   10 | void func(std::vector<double> values);
      |      ^~~~
<source>:11:6: note: candidate: 'void func(std::vector<A>)'
   11 | void func(std::vector<A> as);
      |      ^~~~
Compiler returned: 1
Run Code Online (Sandbox Code Playgroud)

(VC15(VS 2017)也拒绝了这个例子。)


哪一个是正确的?为什么?

use*_*570 4

太棒了;

该程序格式不正确func({4.2}),因为在第二个重载的调用的重载解析期间,选择了需要 a 的func(std::vector<A> as)显式构造函数,这在复制列表初始化中是不允许的。std::vectorsize_t

语言-律师讲解

步骤1

func({4.2})首先让我们考虑针对第一个重载的调用的重载决策func(std::vector<double>)

请注意,这func({ 4.2 })复制初始化

以大括号或等于初始化器或条件 ([stmt.select]) 的 = 形式发生的初始化,以及参数传递、函数返回、抛出异常 ([ except.throw])、处理异常([except.handle])和聚合成员初始化(除指定初始化子句([dcl.init.aggr])之外)称为复制初始化。

现在我们转到dcl.init.general#16来看看这将使用列表初始化

初始化器的语义如下。目标类型是正在初始化的对象或引用的类型,源类型是初始化表达式的类型。如果初始值设定项不是单个(可能带括号)表达式,则未定义源类型。

  • 如果初始化器是(非括号)braced-init-list或 is = braced-init-list,则对象或引用是列表初始化的 ([dcl.init.list])

所以从dcl.init.list我们也看到这是copy-list-initialization

列表初始化是对大括号初始化列表中的对象或引用进行初始化。这样的初始值设定项称为初始值设定项列表,初始值设定项列表的逗号分隔初始值设定项子句或指定初始值设定项列表的指定初始值设定项子句称为初始值设定项列表的元素。初始值设定项列表可能为空。列表初始化可以发生在直接初始化或复制初始化上下文中;直接初始化上下文中的列表初始化称为直接列表初始化,而复制初始化上下文中的列表初始化称为复制列表初始化。非列表初始化的直接初始化称为直接非列表初始化。

最后我们进入dcl.init.list#3

T 类型的对象或引用的列表初始化定义如下:

  • 否则,如果 T 是类类型,则考虑构造函数。枚举适用的构造函数,并通过重载决策([over.match]、[over.match.list])选择最好的构造函数。如果需要缩小转换(见下文)来转换任何参数,则该程序格式错误。

这意味着重载决策是通过std::vector参数 ' 来完成的{4.2},并且将选择最好的一个。

所以我们进入over.match.list

当非聚合类类型 T 的对象被列表初始化时,[dcl.init.list] 指定根据本子条款中的规则执行重载决策,或者根据 [over.ics. list],重载决策分两个阶段选择构造函数:

  • 如果初始值设定项列表不为空或 T 没有默认构造函数,则首先执行重载决策,其中候选函数是类 T 的初始值设定项列表构造函数 ([dcl.init.list]),参数列表由初始值设定项组成列为单个参数。
  • 否则,或者如果没有找到可行的初始值设定项列表构造函数,则再次执行重载决策,其中候选函数是类 T 的所有构造函数,参数列表由初始值设定项列表的元素组成。

请注意,由于找到了初始值设定项列表构造函数,因此不会再次执行重载决策。这反过来意味着初始化列表构造器是func({4.2})与第一个重载匹配时选择的选项func(std::vector<double>)

第2步

现在我们看看如何func({4.2})与第二个重载相匹配func(std::vector<A>)

在这种情况下,几乎所有步骤都是相同的(与最后一种情况相同),除了这次初始化器列表构造函数不可行std::vector(std::initializer_list<A>),因此满足语句if no validinitializer-list constructor is found,重载决策再次执行并且所以

否则,或者如果没有找到可行的初始值设定项列表构造函数,则再次执行重载决策,其中候选函数是类 T 的所有构造函数,参数列表由初始值设定项列表的元素组成。

这意味着这一次,将选择std::size_t的参数 ctor 。std::vector但请注意,这个 ctor 是std::vector并且explicit我们有:

在复制列表初始化中,如果选择显式构造函数,则初始化格式错误。

size_t因此,参数 ctor的选择std::vector使得程序格式错误

  • [dcl.init.list] 在这里没有作用。首先发生重载解析,这需要根据 [over.ics.list] 形成隐式转换序列,然后比较它们。两者都可以形成,并且没有一个比另一个更好,因此由于歧义性,这是不正确的。我们永远不会真正执行初始化,这是我们使用 [dcl.init.list] 中的规则的地方。 (2认同)