可以以便携式方式使用新的数组放置吗?

Posted

技术标签:

【中文标题】可以以便携式方式使用新的数组放置吗?【英文标题】:Can placement new for arrays be used in a portable way? 【发布时间】:2010-09-06 03:03:23 【问题描述】:

在将可移植代码用于数组时,是否可以实际使用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[] 返回的地址与我传入的地址相同。

【问题讨论】:

啊诅咒。我欺骗了你的问题:(Array placement-new requires unspecified overhead in the buffer? 嗯...如果删除虚拟析构函数时开销消失了,则表明开销可能来自类的 vtable,或者来自 VStudio 的 RTTI 实现。 或者至少,部分开销是。也有可能仅当类具有非平凡的析构函数时才使用开销。 【参考方案1】:

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

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 时遍历它们,并确保在每个对象上调用析构函数。如果它不知道有多少,它就无法做到这一点。

【讨论】:

VC++ 是一个糟糕的编译器。默认放置新数组又名new(void*) type[n] 执行typen 对象的就地构造。提供的指针必须正确对齐以匹配alignof(type)(注意:由于填充,sizeof(type)alignof(type) 的倍数)。因为您通常必须携带数组的长度,所以实际上不需要将其存储在数组中,因为无论如何您都将使用 for 循环来销毁它(没有放置删除运算符)。 @bit2shift C++ 标准明确声明new[] 填充分配的内存,尽管它允许填充 0 字节。 (参见“expr.new”部分,特别是示例(14.3)和(14.4)以及它们下面的解释。)因此,在这方面,它实际上是符合标准的。 [如果您没有 C++14 标准最终版本的副本,请参阅第 133 页here。] @JustinTime 这是一个似乎没人注意到的缺陷。 @JustinTime 这是标准本身的一个缺陷,允许在 void* operator new[](std::size_t count, void* ptr); 已知该运算符为“无操作”(无分配)时应用开销,并且也清楚地知道此运算符或其标量兄弟返回的指针不能传递给deletedelete[],需要程序员手动销毁每个元素。 破坏应该以相反的构建顺序发生。使循环从 NUMELEMENTS 变为 0。在这种情况下,这无关紧要,但在一般情况下确实如此(后来的对象可能依赖于以前的对象),并且复制粘贴代码的人可能不知道【参考方案2】:

@德里克

5.3.4,第 12 节讨论了数组分配开销,除非我误读它,否则它似乎向我暗示编译器将其添加到新位置也是有效的:

这种开销可能会应用于所有数组 new 表达式,包括那些引用库函数 operator new[](std::size_t, void*) 和其他布局分配函数的表达式。每次调用 new 时,开销可能会有所不同。

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

【讨论】:

如果 VC 是唯一添加额外空间的编译器,我会感到震惊,我认为所有编译器都会存储要调用的析构函数的数量。没有其他合乎逻辑的地方可以放置它。 @MooingDuck 没有放置删除运算符来使用上述“数组长度”。事实上,您必须手动调用带有new(pointer) type[length] 的数组构造 的析构函数。 VC是一个糟糕的编译器,每个人都应该知道。【参考方案3】:

@詹姆斯

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

在考虑了这一点之后,我同意你的看法。没有放置 new 需要存储元素数量的理由,因为没有放置删除。由于没有placement delete,所以placement new 没有理由存储元素的数量。

我还在我的 Mac 上使用带有析构函数的类使用 gcc 对此进行了测试。在我的系统上,placement new 改变了指针。这让我想知道这是否是一个 VC++ 问题,以及这是否可能违反标准(据我所知,该标准并没有专门解决这个问题)。

【讨论】:

我在Coliru 上使用-std=c++14-pedantic 测试了clang 3.7.0 和GCC 5.3.0。两者都没有显示存在开销,特别是对于具有非平凡析构函数的类。所以,我在想,这是 Visual C++ 编译器如此糟糕的另一个例子。 我非常怀疑,@Nik-Lz,考虑到他们仍然炫耀丑陋的 nothrownew.obj 以使 new 表现得像 new(std::nothrow)【参考方案4】:

感谢您的回复。对数组中的每个项目使用新放置是我遇到这个问题时最终使用的解决方案(对不起,应该在问题中提到这一点)。我只是觉得在放置 new[] 时一定有一些我错过的东西。事实上,由于标准允许编译器向数组添加额外的未指定开销,因此放置 new[] 似乎本质上是不可用的。我看不出你怎么能安全便携地使用它。

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

【讨论】:

【参考方案5】:

Placement new 本身是可移植的,但您对它对指定内存块所做的假设是不可移植的。就像之前所说的那样,如果你是一个编译器并且有一块内存,如果你只有一个指针,你怎么知道如何分配一个数组并正确地破坏每个元素? (见operator delete[]接口)

编辑:

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

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

【讨论】:

如果它覆盖了任意大量的存储空间,它怎么可能是“便携的”?你永远无法安全地调用它,因为你永远不知道它会写多少。 delete[] 表达式需要知道,但它只能用于具有堆分配(抛出或nothrow)的new[] 表达式的结果。对于放置new[],确实没有必要。我对 VC 行为的唯一解释是堆 new[] 以某种方式在放置 new[] 方面实现,并且不存储元素本身的数量。【参考方案6】:

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

如果您需要大小用于可能不知道元素数量的其他计算,您可以使用 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();

【讨论】:

重点是在new [] 的情况下,MSVC 显然需要超出sizeof(A[NUMELEMENTS]) 值的额外空间。 sizeof(A[N]) 通常只是 N * sizeof(N),不会反映这个额外的所需空间。【参考方案7】:

我认为 gcc 和 MSVC 做同样的事情,但这当然不能让它“便携”。

当 NUMELEMENTS 确实是编译时间常数时,我认为您可以解决该问题,如下所示:

typedef A Arr[NUMELEMENTS];

A* p = 新(缓冲区)Arr;

这应该使用新的标量放置。

【讨论】:

它没有。 operator new() vs operator new[]() 取决于是否涉及数组类型,而不是源代码中是否有[]

以上是关于可以以便携式方式使用新的数组放置吗?的主要内容,如果未能解决你的问题,请参考以下文章

可以以编程方式使用新的WP7剪切和粘贴功能吗?

HDFView 2.4 使便携

请教,在List集合中怎么得到元素的索引值

能把网页中打开的pdf文件直接保存下来而不使用下载吗?

以编程方式创建有机组

如何转移UIView并使用Swift以编程方式添加新的UIView?