混合运算符new[]和placement new与普通delete[]

Mixing operator new[] and placement new with ordinary delete[]

本文关键字:new delete 运算符 混合 placement      更新时间:2023-10-16

出于好奇,以下内容合法吗?

X* p = static_cast<X*>(operator new[](3 * sizeof(X)));
new(p + 0) X();
new(p + 1) X();
new(p + 2) X();
delete[] p;   // Am I allowed to use delete[] here? Or is it undefined behavior?

类似:

X* q = new X[3]();
(q + 2)->~X();
(q + 1)->~X();
(q + 0)->~X();
operator delete[](q);

我很确定两者都给出UB。

§5.3.4/12表示,新表达式的数组形式可能会在分配的内存量上增加一些任意数量的开销。然后,数组删除可以/可以用它期望的额外内存做一些事情,但不是这样,因为你没有分配它期望的多余空间。至少,它通常至少会补偿预期分配的额外内存量,以返回到它认为从operator new返回的地址——但由于您没有分配额外内存或应用偏移量,当它这样做时,它会传递一个指向operator delete[]的指针,而该指针不是从operator new[]返回的,导致UB(事实上,甚至试图在返回地址的开头之前形成地址,从技术上讲是UB)。

同一节说,如果它分配了额外的内存,它必须将返回的指针偏移该开销。当/如果使用从新表达式返回的指针调用operator delete[]而不补偿偏移量时,则使用与返回的operator new[]不同的指针来调用operator delete[],从而再次给出UB。

§5.3.4/12是一个非规范性注释,但我在规范性文本中没有看到任何与之相矛盾的内容。

从n3242:中的5.3.5[expr.delete]

2

[…]

在第二个备选方案中(删除数组),的操作数的值delete可以是空指针值,或者由上一个数组的新表达式。如果不是,行为是未定义的。[…]

这意味着对于delete[] pp必须是形式为new[] p(新表达式)或0的结果。鉴于这里没有列出operator new的结果,我认为第一个案例是正确的。


我相信第二种情况是可以的。从18.6.1.2【new.delete.array】开始:

11

void operator delete[](void* ptr) noexcept;

[…]

要求:ptr应为空指针或其值应为通过先前对的调用返回操作员新或运算符new[](std::size_t,conststd::nothrow_t&)还没有由于对的干预调用而无效操作员删除。[…]

(3.7.4.2【basic.stc.dynamic.deallocation】第3段中有类似的文本)

因此,只要解/分配函数匹配(例如delete[] (new[3] T)格式良好),就不会发生任何不良情况[还是?见下文]


我想我在5.3.4[expr.new]中跟踪了Jerry警告的规范文本:

10

新表达式传递请求分配的空间函数作为类型的第一个参数std::size_t。这个论点是否定的小于对象的大小创建;它可能大于仅创建对象的大小如果对象是数组。[…]

在同一段中,下面是一个示例(非常不规范),它强调了实现的新表达式确实可以自由地从分配函数中要求比数组占用的空间更多的空间(脑海中浮现出存储可用于解除分配函数的可选std::size_t参数),并且它们可以偏移到结果中。因此,在数组的情况下,所有的赌注都被取消了。不过,非阵列情况似乎不错:

auto* p = new T;
// Still icky
p->~T();
operator delete(p);

如果它们不是UB,它们应该是。在示例1中,您使用的是delete[],其中底层机制不知道要销毁多少对象。如果new[]delete[]的实现使用cookie,则此操作将失败。示例2中的代码假设地址q是传递给operator delete[]的正确地址,而在使用cookie的实现中则不是这样。

我认为这不合法。因为这意味着这些方程式:

new-expression    = allocation-function  +  constructor
delete-expression = destructor  +  deallocation-function

没有更多,没有更少。但据我所知,标准并没有这么说。new-expression可能比allocation-function + constructor加起来做得更多。也就是说,实际的方程可能是这样的,而标准并没有明确禁止它:

new-expression    = allocation-function  +  constructor   +  some-other-work
delete-expression = destructor  +  deallocation-function  +  some-other-work

正确的是:

X* p = static_cast<X*>(new char[3 * sizeof(X)]);
// ...
delete[] static_cast<char*>(p);

X* p = static_cast<X*>(operator new[](3 * sizeof(X)));
// ...
operator delete[](p);

数组删除表达式的类型必须与新表达式完全匹配。


第一个例子是UB,因为第5.3.5节([expr.delete])说

在第一种选择(删除对象)中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除对象的动态类型的基类,并且静态类型应具有虚拟析构函数或行为未定义。在第二种选择(delete array)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。


我的修正版本是可以的,因为(第3.9节[basic.life]):

程序可以通过重用对象所占用的存储,或者通过显式调用具有非平凡析构函数的类类型对象的析构函数,来结束任何对象的生存期。对于具有非平凡析构函数的类类型的对象,在对象所占用的存储被重用或释放之前,程序不需要显式调用析构函数;但是,如果没有显式调用析构函数,或者没有使用删除表达式(5.3.5)来释放存储,则不应隐式调用析构函数,并且依赖于析构函数产生的副作用的任何程序都已定义行为


第二个例子是不允许的,如果X有一个非平凡的析构函数,因为(也是3.9 [basic.life]):

在对象的生存期开始之前,但在对象将占用的存储空间结束之后分配38,或者,在对象的寿命结束之后,在对象占用的存储之前重复使用或释放,指向对象将要或曾经所在的存储位置的任何指针可以使用,但仅以有限的方式使用。关于正在建造或毁坏的物体,请参见12.7。否则这样的指针指的是所分配的存储(3.7.4.2)并且使用该指针就好像该指针是void*类型一样,定义良好。这样的指针可以被取消引用,但生成的左值只能在有限的情况下使用如下所述。

程序有未定义的行为,如果:

  • 对象将是或曾经是具有非平凡析构函数的类类型,并且指针用作删除表达式的操作数