【问题标题】:C++ Copy constructor gets called instead of initializer_list<>调用 C++ 复制构造函数而不是 initializer_list<>
【发布时间】:2016-12-12 04:59:26
【问题描述】:

基于此代码

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

复制 ctor

复制 ctor

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

取而代之的是复制构造函数。

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

【问题讨论】:

  • 看起来有什么奇怪的事情发生了 g++ 给了this 而clang 给了this

标签: c++ copy-constructor initializer-list list-initialization


【解决方案1】:

正如 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&lt;E&gt; 的特化...

到目前为止浏览列表:

  1. Foo 不是聚合。
  2. 它只有一个元素。
  3. Foo 不是 std::initializer_list&lt;E&gt; 的特化。

然后我们点击 [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} 满足所有这些要求。

【讨论】:

  • 为什么如果我写 Foo c{a,b} 输出首先是“copy ctor”然后是“initializer-list”?
  • @Dunno std::initializer_list 复制其内容。
  • @jaggedSpire 你不是说“默认构造函数”而不是“默认复制构造函数”吗?
  • @jaggedSpire:cpprefence 不正确。有关详细信息,请参阅我的答案,但只要说 GCC 就在这里就足够了。
  • @NicolBolas 我相信当这个问题发布时(2 月),cppreference 反映了 wg21.link/cwg1467 之后但 wg21.link/cwg2137 之前的语言状态。从那以后情况发生了变化。
【解决方案2】:

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

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

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

3.3 不适用,因为Foo 不是initializer_list 的特化。

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

总之,GCC是对的,Clang是错的是错的

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

【讨论】:

  • 看起来这个问题只在评论部分提到了 gcc 和 clang。
  • @EdgarRokyan:是的,但我发现 this 问题是由于另一个问题被标记为该问题的副本。该问题(已被其所有者不必要地删除)有来自 Clang 和 GCC 的示例,显示了它们之间的差异。
  • 老实说,我是楼主 :) 因为和这个太相似所以我删了。
  • @EdgarRokyan:是的,太相似是它被关闭为重复的原因。但是删除它意味着我不能引用它。我无法指出类似的代码并展示 GCC 如何正确而 Clang 错误。
  • 为什么在initializer_list协构函数之前调用了拷贝协构函数? @NicolBolas
猜你喜欢
  • 2016-03-25
  • 2022-11-21
  • 1970-01-01
  • 1970-01-01
  • 2017-08-25
  • 1970-01-01
  • 2015-06-10
  • 2012-06-28
相关资源
最近更新 更多