应该在以下代码中调用哪个构造函数,为什么?
struct S
{
int i;
S() = default;
S(void *) : i{1} { ; }
};
S s{{}};
如果我使用clang
(来自主干),则会调用第二个。
如果第二个构造函数被注释掉,那么S{{}}
仍然是有效的表达式,但是(我相信)在这种情况下会调用默认构造的S{}
实例中的move-constructor。
为什么转换构造函数在第一种情况下优先于默认值?
S
的构造函数的这种组合的意图是保存其std::is_trivially_default_constructible_v< S >
属性,除了一组有限的情况,它应该以某种方式初始化。
答案 0 :(得分:9)
如果第二个构造函数被注释掉,那么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
的构造函数的第一个参数,转换到{ {1}}或引用(可能是cv-qualified)X
,不考虑用户定义的转换序列。
这排除了考虑X
和S(S const&)
作为候选者 - 它们正是这种情况 - 目标是[over.match.list]第二阶段结果的构造函数的第一个参数并且目标是对可能符合cv的S(S&& )
的引用,并且这样的转换序列将由用户定义。
因此,唯一剩下的候选人是S
,所以它通常是最有效的候选人。