【问题标题】:How to in-place initialize an array?如何就地初始化数组?
【发布时间】:2017-07-07 15:49:32
【问题描述】:

如何在不复制或移动构造临时元素的情况下初始化数组?当元素具有明确的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、a2 和 a3 是否算作初始化?例如a1 是一个声明,但它的元素获取初始值,尽管是默认值。
  3. 其中有错误吗?我用 C++14 标志做了这个 GCC 6.3.0。
  4. 为什么复制初始化与零初始化相结合仍然在复制初始化的范畴下工作?
  5. 一般来说,所有数组初始化都是用花括号来构造临时元素(除非在没有删除复制或移动构造函数时省略(或者省略不适用于数组?))通过每个元素的复制、移动或复制和移动构造的混合?

【问题讨论】:

  • Foo a[] = { Foo(5), Foo(5) }; 怎么样?
  • 请注意,C++17 已保证复制省略,因此 C++14 中的一些非良构案例将变为良构。
  • @KerrekSB , Foo a[] = { Foo(5), Foo(5) }; 给error: use of deleted function 'Foo::Foo(const Foo&amp;)'
  • 没有“复制零”和“直接零”初始化之类的东西。这些是复制列表初始化和带有空列表的直接列表初始化。而且它们绝不是零初始化,因为Foo 将由其构造函数初始化。
  • Visual C++ 2015 update 2 编译直接初始化就好了。

标签: c++ arrays in-place copy-elision deleted-functions


【解决方案1】:

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

在聚合初始化中,= 的存在没有任何区别。它的基本定义在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) 构造函数,这意味着不需要复制构造函数。


为了完整起见,我会提到nuclear option:

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 中,我相信所谓的“保证复制省略”会将复制初始化更改为实际上不需要复制/移动构造函数。一旦标准发布,我希望更新这个答案。草案中的聚合初始化也有一些摆弄。

【讨论】:

  • “就地初始化”是指在构造数组时直接在数组中构造元素。我想要这个:Foo a4[2] {5, 5},但有可能在复制它们之前构造临时的Foos。删除复制 ctor 会中断编译。我这样说是为了与int m[2]; 区分开来,后者给出了初始垃圾值元素,并且需要在事后更改分配。虽然Foo 有一个带有默认参数的ctor,但我这样说是为了强调我想在构造时使用任何客户端值构造数组,而不一定让它使用默认参数。
  • @CodeBricks int m[2]; 没有初始化程序(将被描述为 default-initialization)。后续赋值不是初始化。
  • 我认为“就地初始化”和“初始化”在给数组一个初始值方面没有区别。我的意思是“就地”,类似于std::vector::emplace_back,它将参数转发给元素的构造函数,以直接在向量的内部数组中构造元素;除了在我的堆栈数组的情况下,我希望这个构造在数组的构造过程中发生,而不是在构造后放置新或分配给它。我不想在花括号列表中构造临时元素来复制构造。
  • @mdx 在您的示例中,初始化程序是 {3} 而不是 3。因此,作为一个更简单的最小示例,比较 Foo b = 3; 和 Foo b = {3}; 。在 C++17 之前,前者是一个错误,因为它是复制初始化,相当于 Foo b = Foo(3) 需要复制/移动构造函数。但是Foo b = {3}; 是从一个列表复制初始化,称为copy-list-initialization,其语义与非列表复制初始化有点不同;在这种情况下,列表元素被视为 b 的构造函数参数。如果 Foo 的 ctor 采用 2 个参数,区别可能会更清楚。
  • 顺便说一句,发布后续问题的一个好方法是开始一个新问题并包含一个链接,引用您已跟进的 Q/A
【解决方案2】:

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

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

或

Foo a5[2] {{4},{3}};

【讨论】:

    【解决方案3】:

    您还可以使用 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(plain old data),您可以直接初始化对象数组。为了使类成为 POD,它必须没有构造函数、析构函数或虚拟方法。类中的所有内容也必须声明为 public 才能成为 POD。基本上,POD 类只是一个 c 风格的结构,其中可以包含方法。

    【讨论】:

    • 他的问题是关于类对象数组的。
    • 在 C++ 中,写入 malloc 空间而不使用placement-new 在空间中创建对象是未定义的行为。 Further reading
    • 请查看我评论中链接的答案 - 除非已在存储中创建对象,否则不允许写入分配的存储。 (C++ 在这方面与 C 不同)
    • 当我之前举这个例子时,我的意思是它只适用于原语。
    • 您显然还没有查看我链接到的答案中的代码。
    【解决方案4】:

    我手头没有 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 } } 变体可以编译,因为编译器知道它可以使用提供的 { 5 } 初始化程序构造 Foo 对象,因此不需要临时变量 - 尽管我不知道允许这样做的确切标准措辞。

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

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

    xx

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-10-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-24
      • 2013-02-25
      相关资源
      最近更新 更多