应该在以下代码中调用哪个构造函数,为什么?
struct S
{
int i;
S() = default;
S(void *) : i{1} { ; }
};
S s{{}};
如果我使用 clang
(从主干),然后第二个被调用。
如果第二个构造函数被注释掉,那么 S{{}}
仍然是有效的表达式,但(我相信)move-constructor来自默认构造的实例 S{}
被称为案件。
为什么转换构造函数在第一种情况下优先于默认值?
这种结构的意图是建构者的 S
是为了保存它 std::is_trivially_default_constructible_v< S >
属性,除了有限的一组情况,它应该以某种方式初始化。
如果第二个构造函数被注释掉,那么S {{}}仍然是有效的表达式,但是(我确定)在这种情况下调用默认构造的S {}实例中的move-constructor。
实际上,这不是发生的事情。 [dcl.init.list]中的顺序是:
列表初始化对象或类型T的引用定义如下:
- 如果T是聚合类,并且初始化列表具有cv U类型的单个元素,[...]
- 否则,如果T是一个字符数组并且[...]
- 否则,如果T是聚合,则执行聚合初始化(8.6.1)。
一旦你删除了 S(void *)
构造函数, S
成为聚合 - 它没有用户提供的构造函数。 S() = default
由于原因,不计入用户提供的数量。聚合初始化 {}
将最终价值初始化 i
会员。
为什么转换构造函数在第一种情况下优先于默认值?
随着 void*
剩下的,让我们继续沿着子弹列表:
- 否则,如果初始化列表没有元素[...]
- 否则,如果T是std :: initializer_list的特化,[...]
- 否则,如果T是类类型,则考虑构造函数。列举了适用的构造函数
通过重载决策选择最好的一个(13.3,13.3.1.7)。
[over.match.list]为我们提供了两阶段重载解析过程:
- 最初,候选函数是类T的初始化列表构造函数(8.6.4)和
参数列表由初始化列表作为单个参数组成。
- 如果找不到可行的初始化列表构造函数,则再次执行重载解析,其中
候选函数是类T的所有构造函数,参数列表由元素组成
初始化列表。
如果初始化列表没有元素且T具有默认构造函数,则省略第一阶段。
S
没有任何初始化列表构造函数,所以我们进入第二个项目符号并使用参数列表枚举所有构造函数 {}
。我们有多个可行的构造函数:
S(S const& );
S(S&& );
S(void *);
转换序列在[over.ics.list]中定义:
否则,如果参数是非聚合类X,则每13.3.1.7的重载决策选择一个
X的最佳构造函数C,用于从参数初始化列表中执行类型X的对象的初始化:
- 如果C不是初始化列表构造函数,并且初始化列表具有cv U类型的单个元素,[...]
- 否则,隐式转换序列是a 用户定义的转换序列,第二个标准转换序列是标识转换。
和
否则,如果参数类型不是类:[...] - 如果初始化列表没有元素, 隐式转换序列是身份转换。
那就是 S(S&& )
和 S(S const& )
构造函数是用户定义的转换序列和身份转换。但 S(void *)
只是一种身份转换。
但是,[over.best.ics]有这个额外的规则:
但是,如果目标是
- 构造函数的第一个参数 要么
- 用户定义的转换函数的隐式对象参数
并且构造函数或用户定义的转换函数是候选者
- 13.3.1.3,[...]
- 13.3.1.4,13.3.1.5或13.3.1.6(在所有情况下),或
- 13.3.1.7的第二阶段,当初始化列表只有一个元素本身就是一个初始化列表时,目标是类的构造函数的第一个参数 X
,转换是 X
或参考(可能是合格的) X
,
不考虑用户定义的转换序列。
这不考虑 S(S const&)
和 S(S&& )
作为候选人 - 他们正是这种情况 - 目标是构造函数的第一个参数,因为[over.match.list]的第二阶段,目标是对可能的cv资格的引用 S
,这样的转换序列将由用户定义。
因此,唯一剩下的候选人是 S(void *)
,所以这是最好的可行候选人。