C++ 复制构造函数被调用而不是initializer_list<>



基于此代码

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<> 构造函数。

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

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

正如 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

海湾合作委员会是正确的。

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. 最初,候选函数是类T的初始值设定项列表构造函数 (8.5.4(,参数列表由作为单个参数的初始值设定项列表组成。
  2. 如果未找到可行的初始值设定项列表构造函数,则再次执行重载解析,其中候选函数是类T的所有构造函数,参数列表由初始值设定项列表的元素组成。
因此,在

重载解析的第二阶段,只会在初始值设定项列表构造函数之后考虑复制构造函数。此处应使用初始值设定项列表构造函数。

值得注意的是,[over.match.list] 接着:

如果初始值设定项列表没有元素,并且T具有默认构造函数,则省略第一阶段。在复制列表初始化中,如果选择了显式构造函数,则初始化格式不正确。

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

否则,如果初始值设定项列表具有类型为 E 的单个元素,并且 T 不是引用类型或其引用类型与 E 相关,则对象或引用将从该元素初始化;如果需要缩小转换(见下文(才能将元素转换为 T,则程序格式不正确。

这解释了 CPPreference 在单元素列表初始化中的特殊情况,尽管他们将其放置在比应有的顺序更高的顺序中。


原始答案

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

从 CPP 首选项:

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

如果T是类类型,并且初始值设定项列表具有单个元素 相同或派生的类型(可能符合 CV 条件(,对象是 从该元素初始化(通过复制初始化 复制列表初始化,或通过直接初始化 直接列表初始化(。(从 C++14 开始(

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

让我们在这里研究一下 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。它可以,所以这就是必须称呼的。

简而言之,海湾合作委员会是对的,而Clang是错的

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

相关内容

  • 没有找到相关文章

最新更新