双支撑初始化

时间:2017-03-03 19:21:45

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

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

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

1 个答案:

答案 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

     

不考虑用户定义的转换序列。

这排除了考虑XS(S const&)作为候选者 - 它们正是这种情况 - 目标是[over.match.list]第二阶段结果的构造函数的第一个参数并且目标是对可能符合cv的S(S&& )的引用,并且这样的转换序列将由用户定义。

因此,唯一剩下的候选人是S,所以它通常是最有效的候选人。