代码之家  ›  专栏  ›  技术社区  ›  James Sutherland

阵列的新布局可以以便携的方式使用吗?

  •  48
  • James Sutherland  · 技术社区  · 17 年前

    在将placement new用于数组时,是否有可能在可移植代码中实际使用它?

    从new[]返回的指针似乎并不总是与传入的地址相同(5.3.4,标准中的注释12似乎证实了这是正确的),但如果是这样的话,我不明白如何为数组分配缓冲区。

    以下示例显示了该问题。使用Visual Studio编译时,此示例会导致内存损坏:

    #include <new>
    #include <stdio.h>
    
    class A
    {
        public:
    
        A() : data(0) {}
        virtual ~A() {}
        int data;
    };
    
    int main()
    {
        const int NUMELEMENTS=20;
    
        char *pBuffer = new char[NUMELEMENTS*sizeof(A)];
        A *pA = new(pBuffer) A[NUMELEMENTS];
    
        // With VC++, pA will be four bytes higher than pBuffer
        printf("Buffer address: %x, Array address: %x\n", pBuffer, pA);
    
        // Debug runtime will assert here due to heap corruption
        delete[] pBuffer;
    
        return 0;
    }
    

    查看内存,编译器似乎正在使用缓冲区的前四个字节来存储其中项目的数量。这意味着,由于缓冲区仅 sizeof(A)*NUMELEMENTS 数组中的最后一个元素被写入未分配的堆中。

    所以问题是,为了安全地使用placement new[],你能找出你的实现需要多少额外的开销吗?理想情况下,我需要一种在不同编译器之间可移植的技术。请注意,至少在VC的情况下,不同类的开销似乎不同。例如,如果我删除了示例中的虚拟析构函数,则new[]返回的地址与我传入的地址相同。

    8 回复  |  直到 17 年前
        1
  •  32
  •   Stack Exchange Supports Israel    10 年前

    就我个人而言,我会选择不在数组上使用placement new,而是在数组中的每个项目上单独使用placement new。例如:

    int main(int argc, char* argv[])
    {
      const int NUMELEMENTS=20;
    
      char *pBuffer = new char[NUMELEMENTS*sizeof(A)];
      A *pA = (A*)pBuffer;
    
      for(int i = 0; i < NUMELEMENTS; ++i)
      {
        pA[i] = new (pA + i) A();
      }
    
      printf("Buffer address: %x, Array address: %x\n", pBuffer, pA);
    
      // dont forget to destroy!
      for(int i = 0; i < NUMELEMENTS; ++i)
      {
        pA[i].~A();
      }    
    
      delete[] pBuffer;
    
      return 0;
    }
    

    无论使用哪种方法,在删除pBuffer之前,请确保手动销毁数组中的每个项目,因为最终可能会出现泄漏;)

    笔记 :我还没有编译这个,但我认为它应该可以工作(我在一台没有安装C++编译器的机器上)。它仍然表明了这一点:)希望它在某种程度上有所帮助!


    编辑:

    它需要跟踪元素数量的原因是,当你对数组调用delete时,它可以迭代它们,并确保对每个对象调用析构函数。如果它不知道有多少人,它就无法做到这一点。

        2
  •  5
  •   James Sutherland    17 年前

    @德里克

    5.3.4,第12节讨论了数组分配开销,除非我误读了它,否则它似乎暗示编译器在放置new时也可以添加它:

    此开销可以应用于所有数组新表达式,包括引用库函数运算符new[](std::size_t,void*)和其他放置分配函数的表达式。开销的大小可能因new的调用而异。

    也就是说,我认为VC是唯一给我带来麻烦的编译器,其中包括GCC、Codewarrior和ProDG。不过,我必须再次检查才能确定。

        3
  •  4
  •   Derek Park    17 年前

    @詹姆斯

    我甚至不太清楚为什么它需要额外的数据,因为无论如何你都不会在数组上调用delete[],所以我不完全明白为什么它需要知道其中有多少项。

    经过深思熟虑,我同意你的观点。没有理由认为placement new需要存储元素的数量,因为没有placement delete。由于没有place-delete,因此没有理由使用place-new来存储元素的数量。

    我还在Mac上用gcc测试了这个,使用了一个带有析构函数的类。在我的系统中,新的位置是 更改指针。这让我怀疑这是否是一个VC++问题,以及这是否可能违反标准(据我所知,标准没有具体解决这个问题)。

        4
  •  3
  •   James Sutherland    17 年前

    谢谢你的回复。当我遇到这个问题时,我最终使用的解决方案是为数组中的每个项目使用新位置(对不起,应该在问题中提到这一点)。我只是觉得,用placement new来做这件事,我一定错过了什么。事实上,由于标准允许编译器向数组添加额外的未指定开销,placement new[]似乎基本上是不可用的。我不明白你怎么能安全便携地使用它。

    我甚至不太清楚为什么它需要额外的数据,因为无论如何你都不会在数组上调用delete[],所以我不完全明白为什么它需要知道其中有多少项。

        5
  •  3
  •   Tim Cooper    13 年前

    Place-new本身是可移植的,但你对它在指定内存块上的作用所做的假设是不可移植的。就像之前说的那样,如果你是一个编译器,并且有一块内存,如果你只有一个指针,你怎么知道如何分配一个数组并正确销毁每个元素?(见操作员删除界面[]。)

    编辑:

    实际上有一个placement delete,只有当构造函数在分配带有placement new[]的数组时抛出异常时才会调用它。

    new[]是否真的需要以某种方式跟踪元素的数量取决于标准,这取决于编译器。不幸的是,在这种情况下。

        6
  •  2
  •   Andrew Grant    17 年前

    与使用单个元素计算新放置的大小类似,使用这些元素的数组来计算数组所需的大小。

    如果你需要其他计算的大小,其中元素的数量可能未知,你可以使用sizeof(A[1])并乘以你所需的元素计数。

    例如

    char *pBuffer = new char[ sizeof(A[NUMELEMENTS]) ];
    A *pA = (A*)pBuffer;
    
    for(int i = 0; i < NUMELEMENTS; ++i)
    {
        pA[i] = new (pA + i) A();
    }
    
        7
  •  1
  •   Yossi Kreinin    17 年前

    C++17(草案N4659)在[expr.new]第15段中说:

    [O] verhead可以应用于所有阵列 新表达 ,包括引用库函数的那些 operator new[](std::size_t, void*) 以及其他布局分配功能。每次调用的开销可能会有所不同 new 另一个。

    因此,它似乎无法使用 (void*) 安置 new[] 在C++17(及更早版本)中是安全的,我不清楚为什么它被指定存在。

    在C++20(草案N4861)中,这被改为

    [O] verhead可以应用于所有阵列 新表达 ,包括引用位置分配函数的函数,但引用库函数时除外 运算符new[](std::size_t,void*) 每次调用的开销可能会有所不同 另一个。

    因此,如果你确定你使用的是C++20,你可以安全地使用它,但只能使用那一种布局形式,而且只有在你不覆盖标准定义的情况下(它才会出现)。

    即使是C++20文本似乎也很荒谬,因为额外空间的唯一目的是存储数组大小的元数据,但在使用任何自定义放置形式时都无法访问它 。它采用私人格式,仅 delete[] 知道如何读取和使用您无法使用的自定义分配 删除[] ,所以充其量只是浪费空间。

    事实上,据我所知,没有安全的方法来使用自定义表单 operator new[] 完全。无法正确调用析构函数,因为必要的信息没有传递给 操作员新[] 即使你知道这些物体是可以轻易破坏的 表达式可能会返回一个指针,指向您的 操作员新[] 返回(跳过无意义的元数据),因此您无法包装只提供数据的分配库 malloc free 等价物:它还需要一种通过指向块中间的指针来搜索块的方法,即使它存在,也可能会慢得多。

    我不明白他们(或者只是Stroustrup?)是怎么把这件事搞砸的。显然正确的方法是将数组元素的数量和每个元素的大小传递给 操作员新[] 作为两个参数,让每个分配器选择如何存储它。也许我遗漏了什么。