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

68

C++11 Feb草案的5.3.4 [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*)和其他放置分配函数的表达式。开销的大小可能因每次新的调用而异。—示例结束

现在看下面的示例代码:

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

根据上面的引用,第二行new (buffer) std::string[10]会在构建个别的std::string对象之前内部调用operator new[](sizeof(std::string) * 10 + y, buffer)。问题在于如果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*)和其他放置分配函数的表达式。


3
我认为你完全不可能知道这件事。我认为放置新对象(placement new)一直被认为是使用自己的内存管理器的工具,而不是允许你预分配内存的东西。无论如何,为什么不直接用常规的 new 循环遍历数组呢?我认为这不会对性能产生太大影响,因为放置新对象基本上是一个空操作,而数组中所有对象的构造函数都必须单独调用。 - j_kubik
3
@j_kubik 这并不像看起来的那么简单!如果其中一个构造函数在循环过程中抛出异常,你必须清理已经构造的对象,而数组-新的形式则为你完成了这些操作。但是一切似乎表明,放置数组-新可能无法安全使用。 - R. Martinho Fernandes
2
@FredOverflow:非常感谢您澄清问题。 - Mooing Duck
@Adrian:空间的作用很可能是为了让实现知道需要调用多少个析构函数。如果没有这个未指定的空间,delete[] 几乎无法知道有多少个对象。 - Mooing Duck
1
这是有意义的,也是我认为应该这样做的方式。然而,如果是这种情况,它应该是operator new[]operator delete[]的实现细节,在它们所在的任何范围内处理这个额外的开销,而不是将这个开销与最小所需空间一起传递。我认为这是最初的意图,但如果构造函数抛出异常,这可能会导致问题,如果不知道已经构造了多少个元素。C++真正缺少的是定义如何构造元素数组的方法。 - Adrian
显示剩余11条评论
7个回答

51

更新

Nicol Bolas在下面的评论中正确指出,这个问题已经得到修复,对于 operator new[](std::size_t, void* p), 开销总是为零

这个修复作为一个缺陷报告于2019年11月完成,这使它适用于所有版本的C++。

原回答

除非您事先知道这个问题的答案,否则不要使用 operator new[](std::size_t, void* p)。这个答案是实现细节,可能会因编译器/平台而异。尽管对于任何给定的平台通常是稳定的。例如,这是由Itanium ABI规定的。

如果您不知道这个问题的答案,请编写自己的放置数组new操作符,在运行时检查:

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,您可以推断出在您的平台上的y。对于我的平台y是1个字。sizeof(word)的大小取决于我是否为32位或64位架构编译。

1
@Kerrek SB: 你说得对,我的对齐处理不够仔细。我在客户端代码中添加了alignas以纠正它。放置new表达式应该会分别对"cookie"和"data"进行对齐。例如,这是Itanium ABI的做法(http://sourcery.mentor.com/public/cxx-abi/abi.html#array-cookies)。是的,你可以像你建议的那样推断`y`。请注意,`y`可能取决于新类型的对齐方式,以及该类型是否具有平凡析构函数(其他平台可能存在其他细节)。 - Howard Hinnant
5
@HowardHinnant:我仍然感到困惑,为什么放置版本需要任何cookie。它是用来干嘛的?里面有什么东西?毕竟,你唯一能够手动销毁这些数组元素的方式就是手动进行,不是吗?你提供的链接甚至表示放置版本(size_t, void*)没有cookie。您认为cookie的非零性应该成为缺陷报告吗? - Kerrek SB
2
@Kerrek SB:嗯,这是一个好问题,我不确定我有一个好的答案。我想,一些假设的用户编写的放置删除,如果在每个元素的默认构造期间抛出异常,则在清理期间可能会使用cookie。但我没有这样一个例子。即使存在这样一个假设的用户编写的放置删除,它也必须是平台相关的。好的一面是,sizeof(y)为0是合法的。 :-) - Howard Hinnant
2
如果您想提交有关此问题的缺陷报告,应该针对CWG(而不是LWG)。这是CWG问题列表:http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html。提交问题的最佳策略是给该列表的作者发送电子邮件。我不知道如果要求`y == 0`是否总是成功的,即使只是因为与已建立的ABI(例如Itanium ABI)向后兼容性。在这个低级别上破坏ABI非常令人望而生畏。 - Howard Hinnant
5
这件事似乎已经有了一个缺陷报告!唉... - Kerrek SB
显示剩余11条评论

9
更新:经过一些讨论,我明白我的答案已经不适用于这个问题了。我会把它留在这里,但是肯定还需要一个真正的答案。
如果没有找到好的答案,我很乐意用一些赏金来支持这个问题。
我将在这里重新陈述问题,尽我所能地简化它,希望更容易让其他人理解正在被问的内容。问题是:
以下构造是否总是正确的?arr == addr 在结尾处是否成立?
void * addr = std::malloc(N * sizeof(T));
T * arr = ::new (addr) T[N];                // #1

我们从标准中得知,#1 导致调用 ::operator new[](???, addr),其中 ??? 是一个不小于 N * sizeof(T) 的未指定数字,并且我们也知道该调用仅返回 addr 并没有其他影响。我们还知道,arraddr 相应地偏移。我们不知道的是指向 addr 的内存是否足够大,或者我们如何知道要分配多少内存。
您似乎混淆了一些事情:
  1. 您的示例调用了 operator new[]() 而非 operator new()

  2. 分配函数不会构造任何内容。它们只会分配内存。

发生的情况是 表达式 T * p = new T[10]; 导致:
  1. 调用带有大小参数 10 * sizeof(T) + xoperator new[]()

  2. 十次调用类型 T 的默认构造函数,实际上是 ::new (p + i) T()

唯一的特殊之处在于数组-新的 表达式 请求比数组数据本身使用的内存更多。您看不到这些内容,也无法以任何方式利用此信息,只能默默接受。
如果您想知道实际分配了多少内存,可以简单地替换数组分配函数 operator new[]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字节的内存,但你不知道y是多少。


你应该详细说明newoperator new函数之间的区别。在阅读链接对话之前,我认为new只是语法糖。此外,在调用operator new[]而不是operator new时出现了拼写错误。我在这条评论中又犯了同样的错误 :( - Mooing Duck
3
那么new(buf) T[10]又该怎样呢?你如何使buf足够大?(从聊天讨论中得知这实际上是预期的问题,但并不明确:() - R. Martinho Fernandes
3
@GMan:不!相反,我们不知道::new(buf) T[n]需要多少内存!这就是5.3.4中最初的引用所说的:我们调用::operator new[](sizeof(T) * n + y, buf),但并不了解y的大小。 - Kerrek SB
@KerrekSB:我认为你的回答有矛盾之处。首先,对于T * arr = ::new (addr) T[N];,你说“我们也知道arr与addr相应地偏移”,然后你又说T * arr = ::new (buf10) T[10];arr == buf10...这是哪一个呢? - etherice
@etherice:你说得对,我也不确定为什么会写那个。全局分配函数是一个无操作函数,但你无法控制所需空间的数量。因此,buf10需要指向10 * sizeof(T) + y字节的内存,但你无法知道y的值。我会进行编辑。 - Kerrek SB
显示剩余3条评论

7
正如Kerrek SB在评论中提到的,这个缺陷最初是在2004年报告的,2012年得到了解决,解决方案如下:

CWG认为EWG是处理此问题的适当场所。

然后在2013年向EWG报告了这个缺陷,但被关闭为NAD(可能意味着“不是缺陷”),并带有以下注释:

问题在于尝试使用数组new将数组放入现有存储器中。我们不需要使用数组new来实现这一点;只需构造它们即可。

这可能意味着建议的解决方法是对每个要构造的对象使用一次非数组放置new的循环。
此代码的一个未在线程中提到的推论是,对于所有的T,它会导致未定义行为。
T *ptr = new T[N];
::operator delete[](ptr);

即使我们遵循了生命周期规则(即T具有平凡的销毁,或程序不依赖于析构函数的副作用),问题在于ptr已经由于这个未指定的cookie进行了调整,因此将其传递给operator delete[]是错误的值。

7

调用任何版本的operator new[]()都不适用于固定大小的内存区域。实际上,它假定委托给某些真正的内存分配函数,而不仅仅是返回一个指向已分配内存的指针。如果您已经有一个内存区域,并且想要构建对象数组,则应使用std::uninitialized_fill()std::uninitialized_copy()来构建对象(或其他形式的单独构建对象)。

您可能会认为这意味着您还必须手动销毁内存区域中的对象。但是,在从放置的new返回的指针上调用delete[] array不起作用:它将使用非放置版本的operator delete[]()!也就是说,当使用放置的new时,您需要手动销毁对象并释放内存。


1
关于放置运算符delete的好处,@Mooing Duck:请注意。 - Sergey Podobry
1
我知道使用placement-new创建的对象必须手动删除。uninitialized_fill是个好主意,但你似乎在说C++规范中接受缓冲区的数组重载new运算符不能正常工作。这就是你的意思吗?(这是聊天记录得出的结论。) - Mooing Duck
2
placement operator new 正在按照其预期工作:使用附加参数分配内存,并在该内存中构造对象。似乎无法以可移植的方式工作的是仅采用 void* 指向已分配内存的版本。考虑到您不知道对象最终会在哪里,这种方法本身就值得怀疑。 - Dietmar Kühl
2
整个重点在于,只有标准的delete[]运算符需要存储在额外字节中的信息(用于遍历数组,调用每个元素的析构函数,并将数组的大小传递给释放函数,如果需要的话)。对我来说,现在有趣的问题是标准是否确实如此规定,或者我们是否发现了一个缺陷。 - Simon Richter
我不认为这算是一个缺陷。然而,我同意应该增强标准以消除可能使用比对象需要更多的内存的可能性。 - Dietmar Kühl

4
请注意,C++20更改了此答案。 C++17及以前的[expr.new]/11 明确指出,此函数可能获得实现定义的偏移量到其大小:

当new-expression调用分配函数且该分配未被扩展时,new-expression将请求的空间量作为std::size_­t类型的第一个参数传递给分配函数。该参数不得小于正在创建的对象的大小;仅当对象是数组时,它可以大于正在创建的对象的大小。

这允许,但不要求,将提供给数组分配函数的大小增加到sizeof(T) * size
C++20明确禁止此操作。从[expr.new]/15
当一个new表达式调用一个分配函数并且该分配没有被扩展时,new表达式将请求的空间大小作为std::size_t类型的第一个参数传递给分配函数。该参数不得小于正在创建的对象的大小;如果对象是数组并且分配函数不是非分配形式,则它可能大于正在创建的对象的大小。强调添加。即使您引用的非规范性注释也已更改:在所有数组new表达式中都可以应用此开销,包括引用放置分配函数的表达式,但是不适用于引用库函数operator new[](std::size_t,void*)的表达式。

但是其他形式的放置新对象(即非指定的非分配形式)仍然可能会产生开销? - ph3rin

1
这种开销适用于所有数组new-expressions,包括引用库函数operator new[](std::size_t, void*)和其他放置分配函数的表达式。
这是标准中的一个缺陷。有传言称他们找不到志愿者来写一个例外(消息 #1173)。 不可替换的数组放置-new不能与delete[]表达式一起使用,因此您需要循环遍历数组并调用每个解构函数
这种开销针对用户定义的数组放置-new函数,它们像常规的T* tp = new T[length]一样分配内存。它们与delete[]兼容,因此带有携带数组长度的开销。

1
在阅读了相应的标准章节之后,我开始认为针对数组类型使用placement new只是无用的想法,而它被标准允许的唯一原因是新操作符被描述的通用方式:

new表达式尝试创建所应用的typeid(8.1)或newtypeid的对象。该对象的类型是分配的类型。这个类型应该是一个完整的对象类型,但不是抽象类类型或其数组(1.8、3.9、10.4)。[注:因为引用不是对象,所以不能通过new表达式创建引用。][注:typeid可以是cv限定类型,在这种情况下,new表达式创建的对象具有cv限定类型。]

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) )

在我看来,数组放置新似乎只是源于定义的紧凑性(所有可能的用途作为一个方案),并且似乎没有什么好理由禁止它。

这使我们处于这样一种情况:我们有一个无用的运算符,在知道需要多少内存之前需要分配内存。我所能想到的唯一解决方案要么是过度分配内存并希望编译器不需要更多,要么在重写的数组放置新函数/方法中重新分配内存(这实际上违背了使用数组放置新的初衷)。


回答 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一样)


1
然而,没有放置删除,所以最后一个参数并不起作用... - Kerrek SB
这是正确的,但我猜无论是否放置,它仍应在内存中产生相同的二进制结构。 - j_kubik
根本不是这样!二进制结构并没有在任何地方被规定,而且对于所有标准数组来说甚至都不相同——它取决于类型。 - Kerrek SB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接