为啥就地成员初始化在 C++11 中使用复制构造函数?

Posted

技术标签:

【中文标题】为啥就地成员初始化在 C++11 中使用复制构造函数?【英文标题】:Why does an in-place member initialization use a copy constructor in C++11?为什么就地成员初始化在 C++11 中使用复制构造函数? 【发布时间】:2014-03-09 15:18:18 【问题描述】:

我对下面的代码有点困惑:

struct A 
  std::atomic<int> a = 0;
;

这会报错:

复制“std::atomic”类型的成员子对象调用已删除的构造函数

但几乎相同的代码确实有效:

struct A 
  std::atomic<int> a = 0;
;

好吧,如果第一个变体需要复制构造函数,那么它必须使用operator=()。可是等等!此运算符在没有复制构造函数的情况下完美工作:

A a;
a.a = 1;

谁能解释一下这两种就地初始化是如何在简单操作方面进行扩展的?为什么第一个需要复制构造函数?

【问题讨论】:

请记住,复制赋值不使用复制构造函数。 std::atomic&lt;int&gt; a = 0 涉及复制构造函数。 a.a = 1 只是分配,甚至不是复制分配。不过我无法回答,因为我不知道为什么 std::atomic&lt;int&gt; a = 0 会成功,我本来预计会失败。 @MooingDuck 我一直认为,std::atomic&lt;int&gt; a = 0 应该等于 std::atomic&lt;int&gt; a(0) - 没有复制构造函数,但看起来不是。 std::atomic&lt;int&gt; a = 0 将需要一个复制构造函数在语法上,因此 C++ 规范说此语法需要复制构造函数可用。但是,使用复制构造函数将是不必要的开销,因此 C++ 规范还说可以省略复制构造函数,这使其 表现std::atomic&lt;int&gt; a(0); 相同。请注意,此行为仍需要复制构造函数理论上可用 @MooingDuck 感谢您的解释,我不知道。您的评论包含一半的答案。 @MooingDuck 它并不严格要求复制构造函数,移动构造函数足以进行复制初始化。 【参考方案1】:

所有引用均指向 N3797,C++1y 当前工作草案。 §8.5 初始化器 [dcl.init]/15 状态:

表单中发生的初始化

T x = a;

以及参数传递、函数返回、抛出异常 (15.1)、处理异常 (15.3),聚合成员初始化 (8.5.1) 称为 copy-initialization。 [注意:复制初始化可能会调用移动(12.8)。 ——尾注]

所以声明:

std::atomic<int> a = 0;

正在执行复制初始化。根据 8.5/17:

初始化器的语义如下。 目标类型是被初始化的对象或引用的类型,源类型是初始化表达式的类型。如果初始化器不是单个(可能是带括号的)表达式,则源类型未定义。

这里的目标类型std::atomic&lt;int&gt;源类型int(即decltype(0))。为了确定初始化的语义,我们必须确定第 17 段中的哪一个适用:

如果初始化程序是(非括号)braced-init-list,则对象或引用是列表初始化的 (8.5.4)。 如果目标类型是引用类型,请参见 8.5.3。 如果目标类型是字符数组、char16_t 数组、char32_t 数组或wchar_t 数组,并且初始值设定项是字符串文字,请参阅 8.5.2。李> 如果初始值设定项是(),则对象是值初始化的。 否则,如果目标类型是数组,则程序格式错误。 如果目标类型是(可能是 cv 限定的)类类型: 如果初始化是直接初始化,或者如果是复制初始化,其中源类型的 cv 非限定版本与目标类相同或派生类,... [不适用,来源类型为int] 否则(即,对于剩余的复制初始化情况),用户定义的转换序列可以从源类型转换为目标类型,或者(当转换函数 使用) 对其派生类进行枚举,如 13.3.1.4 中所述,并通过重载决议 (13.3) 选择最佳的一个。如果转换无法完成或不明确,则 初始化格式不正确。以初始化表达式作为参数调用所选函数;如果函数是构造函数,则调用初始化 cv-unqualified 的临时 目标类型的版本。临时是prvalue。调用的结果(即 根据上面的规则,构造函数情况下的临时)然后用于直接初始化, 作为复制初始化目标的对象。在某些情况下,实现 允许通过构造 中间结果直接放入正在初始化的对象中;见 12.2、12.8。 ...

我们到了。初始化表达式 - 0 - 通过创建使用 std::atomic&lt;int&gt;(int) 构造函数初始化的临时对象转换为 std::atomic&lt;int&gt;。该临时对象用于直接初始化原始std::atomic&lt;int&gt; 对象。我们之前忽略的另一个“(可能是 cv 限定的)类类型”项目符号现在适用:

如果初始化是直接初始化,或者如果是复制初始化,其中源类型的 cv 非限定版本与目标类相同或派生类,则考虑构造函数。枚举了适用的构造函数(13.3.1.3),并通过重载决议(13.3)选择最佳构造函数。调用如此选择的构造函数来初始化对象,使用初始化表达式或 expression-list 作为其参数。如果没有构造函数适用,或者重载决议不明确,则初始化格式错误。

回想一下,新的初始值设定项是纯右值std::atomic&lt;int&gt;。重载解析确定没有合适的std::atomic&lt;int&gt; 构造函数接受单个参数std::atomic&lt;int&gt;&amp;&amp;std::atomic&lt;int&gt; 不可移动或可复制)并诊断程序为格式错误。

对于问题的第二部分,

std::atomic<int> a = 0;

再次按照 8.5/15 进行复制初始化。然而,这一次,8.5/17 的第一个项目符号适用:

如果初始化程序是(非括号)braced-init-list,则对象或引用是列表初始化的 (8.5.4)。

对于list-initialization,我们必须看8.5.4/3:

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

如果 T 是聚合,则执行聚合初始化 (8.5.1)。 否则,如果初始值设定项列表没有元素且T 是具有默认构造函数的类类型,则该对象是值初始化的。 否则,如果Tstd::initializer_list&lt;E&gt;的特化,则按如下所述构造prvalueinitializer_list对象并用于根据从相同类型的类中初始化对象的规则来初始化对象(8.5)。 否则,如果T 是类类型,则考虑构造函数。枚举适用的构造函数,并通过重载决议(13.3、13.3.1.7)选择最佳构造函数。如果需要缩小转换(见下文)来转换任何参数,则该程序是非良构的。 ...

std::atomic&lt;int&gt; 是类类型,不是聚合或initializer_list 特化,因此考虑构造函数。 std::atomic&lt;int&gt;::atomic(int) 构造函数将被选为完美匹配并用于初始化对象。

【讨论】:

您想要标准参考?有一个语言律师的答案。 遗憾的是,相关参考资料似乎丢失了? §9.2 ad 4 “大括号或等号初始化器应仅出现在数据成员的声明中。(对于静态数据成员,请参阅 9.4.2;对于非静态数据成员,请参阅 12.6.2)。”? @sehe 两个非静态数据成员初始化器的行为差异与它们是非静态数据成员初始化器这一事实无关。 阅读完所有内容后,我同意。但是,它相关上下文,并且语义相同的事实仅在 §12.6.x 中指定(大约 3 个位置)。很高兴明确提及这一点。 (你已经获得了我的 +1) 没有等号的std::atomic&lt;int&gt; a 0 呢?我对[dcl.init] 的解读是,这是list-initialization,相当于= 0。但是 GCC 似乎区分了这两者,由于删除了复制构造函数,导致没有等号的编译错误,而 Clang 没有。【参考方案2】:

让我们考虑第一种情况

struct A 
  std::atomic<int> a = 0;
;

要使初始化成功,需要有一个可访问的复制构造函数。但是复制构造函数被定义为已删除。

atomic(const atomic&) = delete;

所以编译器会报错。

第二种情况

struct A 
  std::atomic<int> a = 0;
;

如果使用初始化列表,则不需要复制构造函数。编译器搜索一个接受一个int参数的构造函数,并且这样的构造函数确实存在,所以它被调用了。

constexpr atomic(T) noexcept;

或者如果用模板参数替换int类型

constexpr atomic(int) noexcept;

根据 C++ 标准,如果一个类没有第一个参数类型为 std::initializer_list 的构造函数(当指定了初始化列表时)那么

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

否则,如果 T 是类类型,则考虑构造函数。这 枚举适用的构造函数并选择最好的构造函数 通过重载决议(13.3、13.3.1.7)。如果收窄 转换(见下文)需要转换任何参数, 程序格式不正确。

最后一种情况

A a;
a.a = 1;

这使用赋值运算符

T operator=(T) noexcept;

或者如果用模板参数替换int类型

int operator=(int) noexcept;

所以没有问题。

【讨论】:

我认为底部的赋值运算符有点模棱两可,如果将其更改为int operator=(int) noexcept,我个人会更高兴,这表明它不是复制构造函数。想法? @Mooing Duck 你的意思是我应该使用参数 int 而不是模板参数? 对不起,但在我看来,您只是用英语表达了编译器的行为:) 您没有解释,为什么编译器会以这种方式而不是另一种方式运行。实际上,为什么第二种情况不需要复制构造函数? @abyss.7 在第二种情况下,由于构造函数如何处理初始化列表的规则,不使用复制构造函数。我会更新我的帖子。 @abyss.7:这些是 C++ 规范中的规则,你想要引号吗?如果这是您所期望的,我们可以做到。

以上是关于为啥就地成员初始化在 C++11 中使用复制构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

复制构造函数中的c ++用户定义成员

在构造函数初始化列表中初始化成员数组(C++11之前)

为啥编译器在尝试使用 C++11 样式初始化对象数组时隐式删除​​构造函数

为啥在默认成员初始化时内置类型的零成员? [复制]

什么是 C++ 中的就地构造函数? [复制]

为啥我不能访问派生构造函数的成员初始化列表中继承的受保护字段?