C++ 初始值列表初始化失败,拷贝构造函数被调用

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;
}

输出结果为:

默认构造函数

复制构造函数

复制构造函数

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

然而,复制构造函数却起到了主导作用。

有人能告诉我为什么会这样吗?


3
看起来有些奇怪,g++编译器给出了这个链接,而clang编译器则给出了这个链接 - NathanOliver
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. 否则,如果T是std::initializer_list的特化...
接下来遍历列表:
1. Foo不是一个聚合体。 2. 它有一个元素。 3. Foo不是std::initializer_list的特化。
然后我们看到[dcl.init.list]/3.4:
否则,如果T是类类型,则考虑构造函数。逐个列举适用的构造函数,并通过重载决议(13.3、13.3.1.7)选择最佳构造函数。如果需要将任何参数进行缩小转换(请参见下文),则程序无效。
现在我们有了一些线索。13.3.1.7也称为[over.match.list]:
列表初始化的初始化方式:
当非聚合类类型T的对象进行列表初始化(8.5.4)时,重载决议分两个阶段选择构造函数:
  1. 最初,候选函数是类T的初始化列表构造函数(8.5.4),参数列表由初始化列表作为单个参数组成。
  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} 满足所有这些要求。


1
@jaggedSpire 你是不是指的是「默认构造函数」而不是「默认复制构造函数」? - Morwenn
如果你正在使用gcc,这是一个bug,并且在gcc bug跟踪器上有记录 - jaggedSpire
@gedamial 不正确。实际上调用初始化列表构造函数的规则相当复杂,但它们确实很好地列在了列表中。无论如何,clang在这种情况下显示了调用复制构造函数的预期行为。要弄清楚这里应该发生什么,请按照列表进行操作:第一条语句适合这种情况:方括号内有一个类型为Foo的单个元素,使用直接列表初始化... - jaggedSpire
1
@jaggedSpire:cppreference 是错误的。详见我的回答,但可以简单地说,这里 GCC 是正确的。 - Nicol Bolas
2
@NicolBolas 我相信在这个问题发布的时候(二月份),cppreference反映了http://wg21.link/cwg1467之后但是http://wg21.link/cwg2137之前的语言状态。自那以后它已经改变了。 - Cubbi
显示剩余12条评论

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_list构造函数,编译器必须检查是否可以从给定的值中制造出与这些构造函数之一相匹配的initializer_list。它可以,所以必须调用那个
简而言之,GCC是正确的,而Clang是错误的。
值得注意的是,C++17工作草案对此没有做任何更改。它有一个新的3.1节,针对单值列表有特殊措辞,但仅适用于聚合体Foo不是一个聚合体,因此不适用。

@EdgarRokyan:没错,但是我是通过另一个被标记为重复的问题找到了这个问题。那个问题(被其所有者不必要地删除了)有来自Clang和GCC的示例,展示了它们之间的差异。 - Nicol Bolas
@EdgarRokyan:是的,因为太相似了,所以它被关闭了。但是删除它意味着我无法引用它。我无法指出类似的代码,并展示GCC如何正确处理,而Clang则处理错误。 - Nicol Bolas
为什么复制构造函数会在初始化列表构造函数之前被调用?@NicolBolas - gedamial
@gedamial:因为Clang有一个bug。标准明确指出应该调用initializer_list构造函数。如果你的编译器没有这样做,那么它就是有缺陷的。 - Nicol Bolas
@NicolBolas 不,我在谈论GCC。Foo c{b}会产生对复制构造函数的调用然后是initializer_list构造函数。 - gedamial
显示剩余5条评论

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