如果注释掉第二个构造函数,则S{}仍然是一个有效的表达式,但在这种情况下会调用来自默认构造的S {}实例的移动构造函数。
实际上,情况并非如此。在[dcl.init.list]中的排序方式是:
T类型的对象或引用的列表初始化定义如下:
- 如果T是聚合类,并且初始化器列表具有单个类型为cv U的元素,则[...]
- 否则,如果T是字符数组,则[...]
- 否则,如果T是一个聚合体,则执行聚合体初始化(8.6.1)。
一旦您删除了构造函数,
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进行重载决议选择单个最佳构造函数C来执行从参数初始化程序列表到类型X对象的初始化:
- 如果C不是初始化程序列表构造函数并且初始化程序列表具有cv U类型的单个元素[...]
- 否则,隐式转换序列是带有第二个标准转换序列为标识转换的用户定义转换序列。
并且
否则,如果参数类型不是类:[...] - 如果初始化程序列表没有元素,则隐式转换序列是标识转换。
也就是说,S(S&&)和S(const S&)构造函数都是用户定义转换序列加上标识转换。但是,S(void*)只是一个标识转换。
但是,[over.best.ics]有这个额外规则:
然而,如果目标是
- 构造函数的第一个参数或
- 用户定义转换函数的隐式对象参数
并且构造函数或用户定义转换函数是候选者之一,由于
- 13.3.1.3,当[...]时
- 13.3.1.4、13.3.1.5或13.3.1.6(在所有情况下),或
- 当初始化程序列表恰好有一个元素本身是初始化程序列表,并且目标是类X的构造函数的第一个参数,并且转换是到X或(可能是cv限定的)X的引用时,13.3.1.7的第二阶段[over.match.list]的结果,用户定义转换序列不会被考虑。
这将S(const S&)和S(S&&)排除在候选者之外 - 它们恰好是这种情况 - 目标作为构造函数的第一个参数由于[over.match.list]的第二阶段而产生,并且目标是引用到可能是cv限定的S,这样的转换序列将是用户定义的。
因此,唯一剩下的候选者是S(void*),因此它是最佳可行候选者。