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

aby*_*s.7 19 c++ initialization atomic copy-constructor c++11

我对以下代码有点困惑:

struct A {
  std::atomic<int> a = 0;
};
Run Code Online (Sandbox Code Playgroud)

这给出了一个错误:

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

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

struct A {
  std::atomic<int> a = {0};
};
Run Code Online (Sandbox Code Playgroud)

Okey,如果第一个变体需要复制构造函数,那么它必须使用operator=().可是等等!这个运算符完美地工作,没有复制构造函数:

A a;
a.a = 1;
Run Code Online (Sandbox Code Playgroud)

任何人都可以解释如何在简单操作方面扩展两个就地初始化吗?为什么第一个需要复制构造函数?

Cas*_*sey 20

所有参考文献都是N3797,C++ 1y当前的工作草案.§8.5初始化器[dcl.init]/15状态:

在表单中发生的初始化

T x = a;
Run Code Online (Sandbox Code Playgroud)

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

宣言如下:

std::atomic<int> a = 0;
Run Code Online (Sandbox Code Playgroud)

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

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

这里的目标类型std::atomic<int>,源类型int(即decltype(0)).要确定初始化的语义,我们必须确定第17段的项目符号中的哪一个适用:

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

我们在那里.初始化表达式 - 通过创建用构造函数初始化的临时对象0转换为a .该临时对象用于直接初始化原始对象.我们之前忽略的另一个"(可能是cv-qualified)类类型"项目符号适用于:std::atomic<int>std::atomic<int>(int)std::atomic<int>

  • 如果初始化是直接初始化,或者它是复制初始化,其中源类型的cv-nonqualified版本与目标类相同的类或派生类,则考虑构造函数.列举了适用的构造函数(13.3.1.3),并通过重载解析(13.3)选择最佳构造函数.调用所选的构造函数来初始化对象,初始化表达式或表达式列表作为其参数.如果没有构造函数适用,或者重载决策是不明确的,则初始化是错误的.

回想一下,新的初始化程序是一个prvalue std::atomic<int>.重载决策确定没有适当的std::atomic<int>构造函数接受单个参数std::atomic<int>&&(std::atomic<int>不可移动或可复制)并将程序诊断为格式错误.

对于问题的第二部分,

std::atomic<int> a = {0};
Run Code Online (Sandbox Code Playgroud)

再次按照8.5/15复制初始化.然而,这一次,8.5/17的第一个子弹适用:

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

对于列表初始化,我们必须查看8.5.4/3:

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

  • 如果T是聚合,则执行聚合初始化(8.5.1).
  • 否则,如果初始化列表没有元素并且T是具有默认构造函数的类类型,则对象将进行值初始化.
  • 否则,如果T是特化std::initializer_list<E>,initializer_list则如下所述构造prvalue 对象,并用于根据相同类型的类初始化对象的规则初始化对象(8.5).
  • 否则,如果T是类类型,则考虑构造函数.枚举适用的构造函数,并通过重载决策(13.3,13.3.1.7)选择最佳构造函数.如果转换任何参数需要缩小转换(见下文),则程序格式错误.
  • ...

std::atomic<int>是类类型,而不是聚合或initializer_list专门化,因此考虑构造函数.该std::atomic<int>::atomic(int)构造将被选定为完美匹配,并用于初始化对象.

  • 你想要标准参考?有一个语言律师的答案. (4认同)

Vla*_*cow 6

让我们考虑第一种情况

struct A {
  std::atomic<int> a = 0;
};
Run Code Online (Sandbox Code Playgroud)

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

atomic(const atomic&) = delete;
Run Code Online (Sandbox Code Playgroud)

所以编译器发出错误.

在第二种情况下

struct A {
  std::atomic<int> a = {0};
};
Run Code Online (Sandbox Code Playgroud)

在使用初始化程序列表的情况下,不需要复制构造函数.编译器搜索接受一个int参数的构造函数,并且这样的构造函数确实存在,因此调用它.

constexpr atomic(T) noexcept;
Run Code Online (Sandbox Code Playgroud)

或者如果要将模板参数替换为int类型

constexpr atomic(int) noexcept;
Run Code Online (Sandbox Code Playgroud)

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

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

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

在最后一种情况下

A a;
a.a = 1;
Run Code Online (Sandbox Code Playgroud)

这使用赋值运算符

T operator=(T) noexcept;
Run Code Online (Sandbox Code Playgroud)

或者如果要将模板参数替换为int类型

int operator=(int) noexcept;
Run Code Online (Sandbox Code Playgroud)

所以没有问题.