17

Just out of curiosity, is the following legal?

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?

Similarly:

X* q = new X[3]();

(q + 2)->~X();
(q + 1)->~X();
(q + 0)->~X();
operator delete[](q);
fredoverflow
  • 237,063
  • 85
  • 359
  • 638
  • Even if it works (which I doubt). It makes the code unmaintainable. Any modification to the class X (like the addition of operator new) is going to need to know about the above code at least for testing purposes. This strong binding of usage of X to the implementation of X is undesirable. – Martin York Jun 27 '11 at 16:58

5 Answers5

7

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.

Jerry Coffin
  • 437,173
  • 71
  • 570
  • 1,035
5

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);
Luc Danton
  • 33,152
  • 5
  • 66
  • 110
  • The second case isn't ok because it uses an object after it has been destroyed. – Ben Voigt Jun 27 '11 at 16:41
  • @BenVoigt What object would that be? – Luc Danton Jun 27 '11 at 16:42
  • @Luc: I just found the paragraph, quoted it at the bottom of my answer. – Ben Voigt Jun 27 '11 at 16:45
  • @BenVoigt As I understand it, that quote *supports* the second case: the pointer *is* used as if a `void*`. – Luc Danton Jun 27 '11 at 16:50
  • @Luc: It specifically says "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". That's the exact case we're discussing... wait, that's a direct call to the deallocation function? In that case it's still UB, because the pointer wasn't gotten from a call to `operator new[]`, but from an *array new expression*. – Ben Voigt Jun 27 '11 at 16:52
  • @BenVoigt I found no relevant requirements on the result of a new expression, and I listed what I think are the relevant requirements for operator delete[]. IMO as long as it's the matching deallocation function to whatever allocation function was used in the new expression, it's not UB. – Luc Danton Jun 27 '11 at 16:55
  • @Ben Still, I'm making the assumption here that the pointer returned by a new expression is the same pointer returned by the allocation function, which Jerry is warning about. So my position is not definitive. – Luc Danton Jun 27 '11 at 17:02
  • @Luc: On what basis do you assume that the result of an array new expression is equal to the pointer returned by the allocation function? That assumption isn't valid, Jerry explains why in his answer. – Ben Voigt Jun 27 '11 at 17:02
2

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
Nawaz
  • 327,095
  • 105
  • 629
  • 812
2

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.

David Hammen
  • 30,597
  • 8
  • 54
  • 98
  • +1, Just thinking about the cookies is clearly the easiest way to understand the validity. Just to clarify, when cookies exist they are added by the compiler; the `operator new[]` and `operator delete[]` functions are none the wiser. – Potatoswatter Jun 27 '11 at 17:46
2

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,
Ben Voigt
  • 260,885
  • 36
  • 380
  • 671