如果第二个构造函数被注释掉,那么 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 类型的单个元素,[...]
— 否则,隐式转换序列是用户定义的转换序列,第二个标准转换序列是恒等转换。
和
否则,如果参数类型不是类:[...] — 如果初始化列表没有元素,隐式转换序列是恒等转换。
也就是说,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或参考(可能是 cv-qualified)X、
不考虑用户定义的转换序列。
这将S(S const&) 和S(S&& ) 作为候选者排除在外——它们正是这种情况——目标是构造函数的第一个参数,因为[over.match.list] 的第二阶段和目标作为对可能 cv 限定的 S 的引用,并且这样的转换序列将是用户定义的。
因此,唯一剩下的候选人是S(void *),所以它是最可行的候选人。