双括号初始化

12
在下面的代码中应该调用哪个构造函数,为什么?
struct S
{
    int i;
    S() = default;
    S(void *) : i{1} { ; }
};

S s{{}};

如果我使用clang(来自trunk),那么第二个被调用。

如果第二个构造函数被注释掉,那么S{{}}仍然是有效的表达式,但在这种情况下(我相信)会从默认构造的S{}实例中调用移动构造函数。

为什么转换构造函数在第一个案例中优先于默认构造函数?

S构造函数的这种组合的意图是保存其std::is_trivially_default_constructible_v< S >属性,除了一些特定情况需要以某种方式初始化之外。


如果第二个构造函数被注释掉,那么 S{{}} 仍然是有效的表达式,但在这种情况下会调用从默认构造的 S{} 实例移动构造函数。不,它使用带大括号初始化的 int 来聚合初始化 S。 - ildjarn
@ildjarn 这个问题仍然存在。 - Tomilov Anatoliy
1个回答

10
如果注释掉第二个构造函数,则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*),因此它是最佳可行候选者。

@T.C 我猜我不确定第二个是标准的转换序列。第一个明确说明为用户定义... - Barry
3
除了一个小细节外,您的分析是正确的:移动构造函数和拷贝构造函数不可行,因为根据16.3.3.1 [over.best.ics]第4段,“当初始化器列表恰好有一个元素本身就是一个初始化器列表,并且目标是类X的构造函数的第一个参数时,并且转换是到X或cv X的引用时......构造函数或用户定义的转换函数是候选函数”在第二阶段[over.match.list]中,不考虑用户定义的转换序列。” - Johannes Schaub - litb
如果在删除用户提供的构造函数时(例如通过拼写默认构造函数的定义),它不是一个聚合,那么删除转换构造函数的示例将无法编译。 - Johannes Schaub - litb
@JohannesSchaub-litb 哎呀,有时候我觉得我懂C++了。但是有时候我又觉得没有人能理解C++。谢谢你指出来,我会修改答案的。 - Barry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接