数组placement-new需要缓冲区中未指定的开销?

时间:2012-01-04 00:03:40

标签: c++ memory-management standards placement-new

5.3.4 C ++ 11月2日草案的[expr.new]给出了示例:

  

new(2,f) T[5]会调用operator new[](sizeof(T)*5+y,2,f)

     

这里,x和y是表示数组分配开销的非负未指定值; new-expression 的结果将从operator new[]返回的值中抵消此金额。此开销可应用于所有数组 new-expressions ,包括引用库函数operator new[](std::size_t, void*)和其他放置分配函数的那些。开销的数量可能因新的一次调用而异。 -end example ]

现在采用以下示例代码:

void* buffer = malloc(sizeof(std::string) * 10);
std::string* p = ::new (buffer) std::string[10];

根据上面的引用,第二行new (buffer) std::string[10]将在内部调用operator new[](sizeof(std::string) * 10 + y, buffer)(在构造单个std::string个对象之前)。问题是如果y > 0,预先分配的缓冲区太小了!

那么我怎么知道在使用数组放置时要预先分配多少内存?

void* buffer = malloc(sizeof(std::string) * 10 + how_much_additional_space);
std::string* p = ::new (buffer) std::string[10];

或者某个地方的标准是否保证y == 0在这种情况下?引用再次说:

  

此开销可应用于所有数组 new-expressions ,包括引用库函数operator new[](std::size_t, void*)和其他放置分配函数的那些。

7 个答案:

答案 0 :(得分:40)

除非您事先知道此问题的答案,否则请勿使用operator new[](std::size_t, void* p)。答案是实现细节,可以随编译器/平台而改变。虽然它对任何给定的平台通常都是稳定的。例如。这是Itanium ABI指定的内容。

如果您不知道此问题的答案,请编写您自己的可在运行时检查此问题的展示位置数组:

inline
void*
operator new[](std::size_t n, void* p, std::size_t limit)
{
    if (n <= limit)
        std::cout << "life is good\n";
    else
        throw std::bad_alloc();
    return p;
}

int main()
{
    alignas(std::string) char buffer[100];
    std::string* p = new(buffer, sizeof(buffer)) std::string[3];
}

通过更改数组大小并检查上面示例中的n,您可以推断出您平台的ymy platform y是1个字。 sizeof(word)取决于我是在编译32位还是64位架构。

答案 1 :(得分:8)

更新:经过一番讨论后,我了解到我的回答不再适用于此问题。我会把它留在这里,但仍然需要一个真正的答案。

如果很快找不到好的答案,我会乐意以一些赏金来支持这个问题。

据我所知,我会在这里重述这个问题,希望更短的版本可以帮助其他人理解被问到的问题。问题是:

以下结构总是正确的吗?最后是arr == addr吗?

void * addr = std::malloc(N * sizeof(T));
T * arr = ::new (addr) T[N];                // #1

我们从标准中知道#1会导致调用::operator new[](???, addr),其中???是一个不小于N * sizeof(T)的未指定数字,我们也知道该调用仅返回{{ 1}}并没有其他影响。我们也知道addr相应地偏离了arr。我们所做的 not 知道的是addr指向的内存是否足够大,或者我们如何知道要分配多少内存。


你似乎混淆了一些事情:

  1. 您的示例调用addr,而不是 operator new[]()

  2. 分配函数不构造任何东西。他们分配

  3. 表达式 operator new()会导致:

    1. 调用T * p = new T[10];,其大小参数为operator new[]()

    2. 十次调用10 * sizeof(T) + x的默认构造函数,实际上是T

    3. 唯一的特点是array-new 表达式要求的内存多于数组数据本身使用的内存。您没有看到任何此类信息,除了默默接受之外,不能以任何方式使用此信息。


      如果您好奇实际分配了多少内存,您可以简单地替换数组分配函数::new (p + i) T()operator new[],并使其打印出实际大小。


      更新:作为随机信息,您应该注意全局展示位置 - 新函数必须是no-ops。也就是说,当你像这样构建一个对象或数组时:

      operator delete[]

      然后对T * p = ::new (buf1) T; T * arr = ::new (buf10) T[10]; ::operator new(std::size_t, void*)的相应调用什么都不做,只返回他们的第二个参数。但是,您不知道::operator new[](std::size_t, void*)应指向的内容:它需要指向buf10字节的内存,但您无法知道10 * sizeof(T) + y

答案 2 :(得分:6)

调用任何版本的operator new[] ()对于固定大小的内存区域都不会很好。本质上,假设它委托一些真正的内存分配函数,而不是只返回指向已分配内存的指针。如果您已经有一个要构建对象数组的内存区域,则需要使用std::uninitialized_fill()std::uninitialized_copy()来构造对象(或者单独构造对象的其他形式)。

您可能会认为这意味着您还必须手动销毁记忆体中的对象。但是,从展示位置delete[] array返回的指针上调用new将无效:它将使用operator delete[] ()的非展示位置版本!也就是说,使用placement new时,您需要手动销毁对象并释放内存。

答案 3 :(得分:5)

正如Kerrek SB在评论中提到的,这个缺陷首先报告in 2004,并在2012年得到解决:

  

CWG同意EWG是处理此问题的适当场所。

然后缺陷是reported to EWG in 2013,但是关闭为NAD(可能意味着&#34; Not A Defect&#34;)并带有评论:

  

问题在于尝试使用array new将数组放入预先存在的存储中。我们不需要使用new new;只是构建它们。

这可能意味着建议的解决方法是使用一个循环,对每个正在构造的对象调用一次非数组放置。

线程上其他地方没有提到的一个推论是,这段代码会导致所有T的未定义行为:

T *ptr = new T[N];
::operator delete[](ptr);

即使我们遵守了生命周期规则(即T要么具有微不足道的破坏,要么程序不依赖于析构函数的副作用),问题是ptr已针对此未指定的Cookie进行了调整,因此传递给operator delete[]是错误的值。

答案 4 :(得分:3)

注意 C++20 改变了这个答案。

C++17's (and before) [expr.new]/11 清楚地表明这个函数可能得到一个实现定义的偏移量到它的大小:

<块引用>

当 new 表达式调用分配函数并且该分配尚未扩展时,new 表达式将请求的空间量作为 std :: size_t 类型的第一个参数传递给分配函数。该参数不应小于正在创建的对象的大小;仅当对象是数组时,它才可能大于正在创建的对象的大小。

这允许,但不要求,数组分配函数的大小可以从 sizeof(T) * size 增加。

C++20 明确禁止这样做。来自[expr.new]/15

<块引用>

当 new 表达式调用分配函数并且该分配尚未扩展时,new 表达式将请求的空间量作为 std :: size_t 类型的第一个参数传递给分配函数。 该参数不应小于正在创建的对象的大小;仅当对象是数组并且分配函数不是非分配形式 ([new.delete.placement]) 时,它才可能大于正在创建的对象的大小。

强调了。甚至您引用的非规范性注释也已更改:

<块引用>

此开销可应用于所有数组 new 表达式,包括引用放置分配函数的表达式,但引用库函数 operator new[](std :: size_t, void*) 时除外。

答案 5 :(得分:1)

在阅读了相应的标准部分后,我觉得数组类型的新位置简直是无用的想法,标准允许它的唯一原因是描述新运算符的通用方法:

  

新表达式尝试创建typeid(8.1)或的对象   应用它的newtypeid。该对象的类型是   分配类型。此类型应为完整的对象类型,但不是   抽象类类型或其数组(1.8,3.9,10.4)。 [注意:因为   引用不是对象,不能创建引用   newexpressions。 ] [注意:typeid可能是一个cvqualified类型,in   在哪种情况下,newexpression创建的对象具有cvqualified   类型。 ]

new-expression: 
    ::(opt) new new-placement(opt) new-type-id new-initializer(opt)
    ::(opt) new new-placement(opt) ( type-id ) new-initializer(opt)

new-placement: ( expression-list )

newtypeid:
    type-specifier-seq new-declarator(opt)

new-declarator:
    ptr-operator new-declarator(opt)
    direct-new-declarator

direct-new-declarator:
    [ expression ]
    direct-new-declarator [ constant-expression ]

new-initializer: ( expression-list(opt) )

对我来说,array placement new似乎只是源于定义的紧凑性(所有可能的用途都是一个方案),似乎没有充分的理由让它被禁止。

这使我们处于这样一种情况:我们有无用的运算符,它需要在知道需要多少内存之前分配内存。我看到的唯一解决方案是分配内存并希望编译器不需要超过提供的内容,或者在重写array placement new函数/方法中重新分配内存(这相当违背了使用array placement new的目的首先)。


回答Kerrek SB指出的问题: 你的例子:

void * addr = std::malloc(N * sizeof(T));
T * arr = ::new (addr) T[N];                // #1

并不总是正确的。在大多数实现中arr!=addr(并且有很好的理由)因此您的代码无效,并且您的缓冲区将被溢出。

关于那些“好理由” - 请注意,在使用array new运算符时,标准创作者会从某些内容管理中释放您,而array placement new在这方面没有什么不同。请注意,您无需通知delete[]有关数组长度的信息,因此必须将此信息保存在数组本身中。哪里?正好在这个额外的记忆中。没有它delete[]'将需要保持数组长度分开(因为stl使用循环和非展示位置new

答案 6 :(得分:0)

  
    

此开销可应用于所有数组 new-expressions ,包括引用库函数delete[]和其他放置分配函数的那些。

  

这是标准中的缺陷。谣言有they couldn't find a volunteer to write an exception to it(消息#1165)。

不可替换的数组展示位置不能与T* tp = new T[length]表达式一起使用,因此您需要遍历数组并调用每个析构函数。< / p>

开销是针对用户定义的数组placement-new 函数,它们像常规delete[]一样分配内存。那些与height: 100%兼容,因此带有数组长度的开销。