18

基于此代码

struct Foo 
{
   Foo() 
   {
       cout << "default ctor" << endl;
   }

   Foo(std::initializer_list<Foo> ilist) 
   {
       cout << "initializer list" << endl;
   }

   Foo(const Foo& copy)
   {
       cout << "copy ctor" << endl;
   }
};

int main()
{

   Foo a;
   Foo b(a); 

   // This calls the copy constructor again! 
   //Shouldn't this call the initializer_list constructor?
   Foo c{b}; 



   _getch();
   return 0;
}

输出是:

默认 ctor

复制者

复制者

在第三种情况下,我将 b 放入应该调用 initializer_list<> 构造函数的大括号初始化中。

相反,复制构造函数带头。

你们中有人能告诉我这是如何工作的吗?为什么?

4

2 回答 2

19

正如 Nicol Bolas 所指出的,这个答案的原始版本是不正确的:在撰写本文时,cppreference 错误地记录了在列表初始化中考虑构造函数的顺序。以下是使用标准的 n4140 草案中存在的规则的答案,该标准非常接近官方 C++14 标准。

原始答案的文本仍包括在内,以备记录。


更新的答案

根据 NathanOliver 的评论,gcc 和 clang 在这种情况下会产生不同的输出:

g++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor
initializer list


clang++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor

gcc 是正确的。

n4140 [dcl.init.list]/1

列表初始化是从一个花括号初始化列表初始化一个对象或引用。

你在那里使用列表初始化,因为c它是一个对象,它的列表初始化规则在 [dcl.init.list]/3 中定义:

[dcl.init.list]/3:

类型 T 的对象或引用的列表初始化定义如下:

  1. 如果T是聚合...
  2. 否则,如果初始化列表没有元素...
  3. 否则,如果Tstd::initializer_list<E>...的专业化

到目前为止浏览列表:

  1. Foo不是聚合。
  2. 它有一个元素。
  3. Foo不是std::initializer_list<E>.

然后我们点击 [dcl.init.list]/3.4:

否则,如果T是类类型,则考虑构造函数。枚举适用的构造函数,并通过重载决议(13.3、13.3.1.7)选择最佳构造函数。如果需要缩小转换(见下文)来转换任何参数,则程序格式错误。

现在我们正在取得进展。13.3.1.7 也称为 [over.match.list]:

通过列表初始化进行初始化
当非聚合类类型的对象T被列表初始化(8.5.4)时,重载决策分两个阶段选择构造函数:

  1. 最初,候选函数是类的初始化列表构造函数(8.5.4),T参数列表由初始化列表作为单个参数组成。
  2. 如果没有找到可行的初始化列表构造函数,则再次执行重载决议,其中候选函数是类的所有构造函数,T参数列表由初始化列表的元素组成。

所以复制构造函数只会在初始化列表构造函数之后被考虑,在重载决议的第二阶段。这里应该使用初始化列表构造函数。

值得注意的是 [over.match.list] 然后继续:

如果初始化列表没有元素并且T有默认构造函数,则省略第一阶段。在复制列表初始化中,如果选择了显式构造函数,则初始化格式错误。

在 [dcl.init.list]/3 之后。5处理单元素列表初始化:

否则,如果初始化列表有一个类型的元素E并且T不是引用类型或其引用类型与引用相关E,则从该元素初始化对象或引用;如果需要缩小转换(见下文)将元素转换为T,则程序格式错误。

这解释了 cppreference 在哪里得到了他们的单元素列表初始化的特殊情况,尽管他们把它放在比它应该的顺序更高的地方。


原始答案

您遇到了列表初始化的一个有趣方面,如果列表满足某些要求,它可能会被视为复制初始化而不是列表初始化。

来自cppreference

类型对象的列表初始化的效果T是:

如果T是类类型并且初始化列表具有相同或派生类型的单个元素(可能是 cv 限定的),则从该元素初始化对象(通过复制列表初始化的复制初始化,或通过直接初始化用于直接列表初始化)。(c++14 起)

Foo c{b}满足所有这些要求。

于 2016-02-08T15:28:49.250 回答
6

让我们在这里检查一下 C++14 规范中关于列表初始化的内容。[dcl.init.list]3 有一系列要按顺序应用的规则:

3.1 不适用,因为Foo不是聚合。

3.2 不适用,因为列表不为空。

3.3 不适用,因为Foo不是initializer_list.

3.4 确实适用,因为Foo是类类型。它说根据 [over.match.list] 考虑具有重载决议的构造函数。这条规则说首先检查构造initializer_list函数。由于您的类型具有构造函数,因此编译器必须检查是否可以根据给定值制造匹配的构造函数之一。可以,所以必须这样称呼initilaizer_listinitializer_list

简而言之,GCC 是对的,Clang是错的

应该注意的是,C++17 工作草案对此没有任何改变。它有一个新的第 3.1 节,其中对单值列表有特殊的措辞,但仅适用于聚合Foo不是聚合,所以它不适用。

于 2016-12-11T16:31:09.473 回答