【问题标题】:Double brace initialization双括号初始化
【发布时间】:2017-07-24 00:47:51
【问题描述】:

在下面的代码中应该调用哪个构造函数,为什么?

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 > 属性,除了有限的情况,它应该以某种方式初始化。

【问题讨论】:

  • "如果第二个构造函数被注释掉,那么 S{{}} 仍然是有效的表达式,但是(我确定)来自 S{} 的默认构造实例的移动构造函数被调用在这种情况下。“不,它使用大括号初始化 int 聚合初始化 S
  • @ildjarn 这个问题仍然有效。

标签: c++ initialization c++14 language-lawyer default-constructor


【解决方案1】:

如果第二个构造函数被注释掉,那么 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 *),所以它是最可行的候选人。

【讨论】:

  • @T.C 我想我不确定第二个是否是标准转换序列。第一个明确声明为用户定义...
  • 您的分析是正确的,除了一点细节:移动和复制构造函数不可行,因为 16.3.3.1[over.best.ics]p4 "并且构造函数或用户定义的转换函数是一个候选... [over.match.list] 的第二阶段,当初始化列表恰好有一个元素本身就是初始化列表,并且目标是类 X 的构造函数的第一个参数,并且转换是到 X 或对 cv X 的引用……不考虑用户定义的转换序列。”
  • 如果他删除用户提供的构造函数时它不是一个聚合(例如通过拼写默认构造函数的定义),那么他删除了转换构造函数的示例将不再编译。
  • @JohannesSchaub-litb OMG。有时我认为我了解 C++。有时我认为没有人了解 C++。会改答案,谢谢指出。
猜你喜欢
  • 2012-08-28
  • 2015-06-04
  • 2010-10-29
  • 2017-10-09
  • 1970-01-01
相关资源
最近更新 更多