7

如何在不复制或移动构造临时元素的情况下初始化数组?当元素具有显式deleted 复制或移动构造函数时,只有当元素具有默认 ctor 或具有所有默认参数的 ctor 并且我执行以下操作之一时,我才能初始化数组:(a)明确声明数组,(b ) 直接初始化和零初始化数组,或 (c) 复制初始化和零初始化数组。直接(但不是零)初始化和复制(但不是零)初始化都不会编译。

struct Foo
{
    Foo(int n = 5) : num(n) {}
    Foo(const Foo&) = delete;
    //Foo(Foo&&) = delete;  // <-- gives same effect
    int num;
};

int main()
{
    // Resultant arrays for 'a1', 'a2', and 'a3' are two
    // 'Foo' elements each with 'num' values of '5':

    Foo a1[2];          // plain declaration
    Foo a2[2] {};       // direct initialization and zero initialization
    Foo a3[2] = {};     // copy initialization and zero initialization
    Foo a4[2] {5, 5};   // direct initialization -> ERROR
    Foo a5[2] = {5, 5}; // copy initialization   -> ERROR
}
  1. 这 3 种方法是在不复制/移动临时元素的情况下初始化数组的唯一方法吗?
  2. a1将,a2a3count 作为初始化吗?ega1是一个声明,但它的元素获取初始值,尽管是默认值。
  3. 他们中的任何一个都是错误吗?我用 C++14 标志做了这个 GCC 6.3.0。
  4. 为什么复制初始化结合零初始化仍然在复制初始化的范畴下工作?
  5. 一般来说,所有带有花括号的数组初始化都只是临时元素的构造(除非在没有删除复制或移动构造函数时省略(或者省略不适用于数组?)),然后是每个元素的复制、移动或混合复制和移动构造?
4

4 回答 4

4

代码声明Foo a2[2];声明了一个数组。初始化数组的唯一方法是通过列表初始化(即用大括号括起来的零个或多个元素的列表),该行为由标准中标题为聚合初始化的部分描述。(术语聚合是指数组和满足特定条件的类)。

在聚合初始化中, 的存在=没有任何区别。它的基本定义在 C++14 [dcl.init.aggr]/2 中:

当聚合由初始化列表初始化时,如 8.5.4 中所指定,初始化列表的元素被视为聚合成员的初始化,按递增的下标或成员顺序。每个成员都是从相应的初始化子句复制初始化的。

另外,/7:

如果列表中的初始化子句比聚合中的成员少,则每个未显式初始化的成员都应从其大括号或相等初始化器初始化,或者,如果没有大括号或相等初始化器,从一个空的初始化列表(8.5.4)。

从中可以看出,复制初始化总是用于每个提供的初始化程序。因此,当初始化程序是表达式时,该类必须存在可访问的复制/移动构造函数。

但是(正如 Anty 所建议的),您可以将初始化程序设置为另一个列表。使用列表的复制初始化称为复制列表初始化:

Foo a6[2] = {{6}, {6}};

当单曲Foo被列表初始化时,它不是聚合初始化(因为Foo不是聚合)。所以规则与上面讨论的不同。非聚合类的复制列表初始化属于列表初始化,在 [dcl.init.list]/3.4 中,它指定Foo列表中的初始化器使用重载解析与构造函数参数匹配。在此阶段Foo(int)将选择构造函数,这意味着不需要复制构造函数。


为了完整起见,我将提到核选项

typename std::aligned_storage< sizeof(Foo), alignof(Foo) >::type buf[2];
::new ((void *)::std::addressof(buf[0])) Foo(5);
::new ((void *)::std::addressof(buf[1])) Foo(5);
Foo *a7 = reinterpret_cast<Foo *>(buf);

// ...
a7[0].~Foo();
a7[1].~Foo();

显然,当您无法通过任何其他方式实现目标时,这是最后的手段。


注 1:以上适用于 C++14。在 C++17 中,我相信所谓的“保证复制省略”会将复制初始化更改为实际上不需要复制/移动构造函数。一旦标准发布,我希望更新这个答案。草案中也有一些关于聚合初始化的问题。

于 2017-02-18T00:34:33.813 回答
4

在您的情况下,您仍然可以使用这些构造:

Foo a4[2] = {{4},{3}};

或者

Foo a5[2] {{4},{3}};
于 2017-02-18T00:41:05.463 回答
1

您还可以使用 malloc 创建一个指针,然后在其上使用数组语法(如果类是 POD)。前任:

class A {
public:
      int var1;
      int var2;
      public int add(int firstNum, int secondNum) {
          return firstNum + secondNum;
      }
}
A * p = 0;
while(!p) {
    p = (A*)malloc(sizeof(A) * 2);
}
p[0] = {2, 3};
p[1] = {2, 5};

还有一种方法可以将数组初始化为临时值,但我忘记了如何做到这一点。

如果类是 POD(普通旧数据),您可以直接初始化对象数组。为了使类成为 POD,它必须没有构造函数、析构函数或虚拟方法。类中的所有内容也必须声明为 public 才能成为 POD。基本上,POD 类只是一个可以在其中包含方法的 c 样式结构。

于 2017-02-18T00:14:50.303 回答
1

我手头没有 C++ 标准,引用它可能是证明我的话的唯一方法。因此,要回答您的每个问题,我只能说:

  1. 不,这还不是全部。我无法为您提供详尽的可能性列表,但我之前肯定使用过以下内容:

xx

 struct Foo
 {
     Foo(int n = 5) : num(n) {}
     Foo(const Foo&) = delete;
     Foo(Foo&&) = delete;
     int num;
 };

int main()
{    
    Foo a1[2];          // plain declaration
    Foo a2[2] {};       // direct initialization
    Foo a3[2] = {};     // also direct initialization
    Foo a4[2] { {5}, {5} };   // also direct initialization
    Foo a5[2] = { {5}, {5} }; // also direct initialization 
}
  1. 大括号初始化不是声明和复制,它是单独的语言结构。它很可能只是就地构造元素。我不确定这是否适用的唯一情况是{ Foo(5), Foo(5) }初始化,因为它明确要求创建临时对象。变体是一样的 { 5, 5},因为为了初始化一个数组,你需要一个用大括号初始化的Foo对象列表。由于您不创建任何内容,因此它将使用临时构造函数来获取{ Foo(5), Foo(5) }. 该{ { 5 }, { 5 } }变体可以编译,因为编译器知道它可以Foo从提供的初始化程序中构造对象{ 5 },因此不需要临时变量——尽管我不知道允许这样做的确切标准措辞。

  2. 不,我认为这些都不是错误。

  3. 我记得 C++ 标准中的一句话,基本上说编译器在创建新变量时总是可以通过直接初始化来替换赋值初始化。

xx

Foo x( 5 );
Foo x { 5 };
Foo x = { 5 }; // Same as above
  1. 正如我在上面已经指出的那样:不,您可以就地初始化数组,您只需要一个适当的元素初始化程序。{ 5 }将被解释为“Foo 对象的初始化程序”,而 plain5将被理解为“可以转换为临时 Foo 对象的值”。初始化器列表通常必须包含元素的初始化器列表,或元素的确切类型的项目。如果给出不同的东西,将创建一个临时的。
于 2017-02-18T13:32:25.323 回答