17

只是出于好奇,以下合法吗?

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

5 回答 5

7

我很确定两者都给UB。

§5.3.4/12 说新表达式的数组形式可能会给分配的内存量增加一些任意数量的开销。数组 delete 可以/可以使用它期望在那里的额外内存做一些事情,但不是因为你没有分配它期望的额外空间。至少它通常至少会补偿它预期分配的额外内存量以返回它认为返回的地址operator new- 但由于您没有分配额外的内存或应用偏移量,当它会传递一个指针operator delete[],指向未从operator new[].

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

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

于 2011-06-27T16:26:16.533 回答
5

从 n3242 中的 5.3.5 [expr.delete] 开始:

2

[...]

在第二种选择(delete array)中,delete 的操作数的值可以是空指针值或由前一个数组 new 表达式产生的指针值。如果不是,则行为未定义。[...]

这意味着 for delete[] p,p一定是某种形式的结果new[] p(一个新的表达式),或者 0。看到operator new这里没有列出的结果,我认为第一种情况是正确的。


我相信第二种情况是好的。从 18.6.1.2 [new.delete.array] 开始:

11

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

[...]

要求: ptr 应为空指针,或其值应为先前调用 operator new 或 operator new[](std::size_t,const std::nothrow_t&) 的返回值,该值未被干预调用无效运算符删除。[...]

(在 3.7.4.2 [basic.stc.dynamic.deallocation],第 3 段中有类似的文字)

因此,只要 de/allocation 函数匹配(例如delete[] (new[3] T)格式正确),就不会发生任何不好的事情。[或者是吗?见下文 ]


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

10

new 表达式将请求的空间量作为 std::size_t 类型的第一个参数传递给分配函数。该参数不应小于正在创建的对象的大小;只有当对象是一个数组时,它才可能大于正在创建的对象的大小。[...]

在同一段后面是一个示例(非常不规范),它强调实现的新表达式确实可以自由地从分配函数中询问比数组占用的空间更多的空间(存储std::size_t可用于释放函数的可选参数)注意),并且它们可以抵消结果。所以在数组的情况下所有的赌注都没有了。不过,非数组情况似乎很好:

auto* p = new T;
// Still icky
p->~T();
operator delete(p);
于 2011-06-27T16:26:03.243 回答
2

我认为这不可能是合法的。因为这意味着这些方程:

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

不多也不少。但据我所知,该标准并没有确切地说明这一点。它可能new-expressionallocation-function + constructor一起做的更多。也就是说,实际的方程式可能是这样的,标准并没有在任何地方明确禁止它:

new-expression    = allocation-function  +  constructor   +  some-other-work
delete-expression = destructor  +  deallocation-function  +  some-other-work
于 2011-06-27T16:29:13.547 回答
2

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

于 2011-06-27T16:31:33.623 回答
2

正确的是:

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]) 节说

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


我的更正版本没问题,因为(第 3.9 节[basic.life]):

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


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

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

如果出现以下情况,该程序具有未定义的行为:

  • 对象将是或曾经是具有非平凡析构函数的类类型,并且指针用作删除表达式的操作数,
于 2011-06-27T16:34:36.457 回答