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

谁说胖子不能爱 提交于 2019-11-29 09:13:42

I'm pretty sure both give UB.

§5.3.4/12 says the array form of a new expression may add some arbitrary amount of overhead to the amount of memory allocated. The array delete can/could then do something with the extra memory it expects to be there, but isn't since you didn't allocate the extra space it expects. At the very least it's normally going to at least compensate for the amount of extra memory it expected to be allocated to get back to the address it believes was returned from operator new -- but since you haven't allocated extra memory or applied an offset, when it does to it'll pass a pointer to operator delete[] that wasn't returned from operator new[], leading to UB (and, in fact, even attempting to form the address before the beginning of the returned address is technically UB).

The same section says that if it allocates extra memory, it has to offset the returned pointer by the amount of that overhead. When/if you call operator delete[] with the pointer that was returned from the new expression without compensating for the offset, you're calling operator delete[] with a pointer that's different from the one operator new[] returned, giving UB again.

§5.3.4/12 is a non-normative note, but I don't see anything in the normative text to contradict it.

From 5.3.5 [expr.delete] in n3242:

2

[...]

In the second alternative (delete array), the value of the operand of delete may be a null pointer value or a pointer value that resulted from a previous array new-expression. If not, the behavior is undefined. [...]

which means that for delete[] p, p must have been the result of something of the form new[] p (a new expression), or 0. Seeing as the result of operator new is not listed here, I think the first case is right out.


I believe the second case is Ok. From 18.6.1.2 [new.delete.array]:

11

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

[...]

Requires: ptr shall be a null pointer or its value shall be the value returned by an earlier call to operator new or operator new[](std::size_t,const std::nothrow_t&) which has not been invalidated by an intervening call to operator delete. [...]

(there is similar text in 3.7.4.2 [basic.stc.dynamic.deallocation], paragraph 3)

So as long as the de/allocation functions match (e.g. delete[] (new[3] T) is well-formed) nothing bad happens. [ or does it? see below ]


I think I tracked the normative text of what Jerry is warning about, in 5.3.4 [expr.new]:

10

A new-expression passes the amount of space requested to the allocation function as the first argument of type std::size_t. That argument shall be no less than the size of the object being created; it may be greater than the size of the object being created only if the object is an array. [...]

Following in the same paragraph is an example (so non-normative) which underlines that the new expressions of an implementation are indeed free to ask more from the allocation function than the space the array takes (storing the optional std::size_t parameter available to deallocation function comes to mind), and that they can offset into the result. So all bets are off in the array case. The non-array case seems fine though:

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

If they aren't UB, they should be. In example 1 you are using delete[] where the underlying mechanism has no clue of how many objects are to be destructed. If the implementation of new[] and delete[] uses cookies, this will fail. The code in example 2 assumes that the address q is the correct address to pass to operator delete[], and this is not the case in an implementation that uses cookies.

I think that cannot be legal. Because that implies these equations:

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

Nothing more, nothing less. But the Standard does not say exactly that, as far as I know. It might be possible that new-expression does more than allocation-function + constructor together do. That is, the actual equations could be this, and the Standard doesn't forbid it explicitly anywhere:

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

Correct would be:

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

or

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

The type of the array delete expression has to match the new expression exactly.


The first example is UB because section 5.3.5 ([expr.delete]) says

In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined. In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.


My corrected version is ok because (section 3.9 [basic.life]):

A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression (5.3.5) is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.


The second example is not allowed iff X has a non-trivial destructor because (also 3.9 [basic.life]):

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated 38 or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined. Such a pointer may be dereferenced but the resulting lvalue may only be used in limited ways, as described below.

The program has undefined behavior if:

  • the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression,
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!