Legality of using delete-expression on a pointer to an object of class type with a trivial destructor/scalar type whose lifetime has ended

Viewed 138

Is the following code legal?

struct S
{};
int main()
{
    S* p = new S;
    p->~S();
    delete p;
}

The standard rules at [basic.life#6]:

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated24 or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of 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 [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.allocation]), and using the pointer as if the pointer were of type void* is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:

(6.1) — 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,

(6.2) — the pointer is used to access a non-static data member or call a non-static member function of the object

[basic.life#6.1] doesn't apply, so this code seems to be legal because S has a trivial destructor.

However, according to [basic.life#6.2], this code is illegal because the delete-expression calls the destructor of the S object, which is a non-static member function of it.

EDIT: According to [class.dtor#19], it's also illegal.

Which should I listen to? Maybe [basic.life#6.2], because [basic.life#6.1] doesn't stipulate that it must be legal to do so for classes with trivial destructor.

The conclusion is that it's illegal.

However, after replacing S with a scalar type, things seem to be different:

#include <memory>
int main()
{
    char* p = new char;
    std::destroy_at(p);
    delete p;
}

This will make a pseudo-destructor call to the char object pointed to by p, which will end its lifetime, according to [expr.call#5]:

If the postfix-expression names a pseudo-destructor (in which case the postfix-expression is a possibly-parenthesized class member access), the function call destroys the object of scalar type denoted by the object expression of the class member access ([expr.ref], [basic.life]).

[basic.life#6.1] still doesn't apply.

But the difference is that [basic.life#6.2] doesn't apply either, because there is no non-static member function called for the char object (char isn't a class after all).

The conclusion is that it's legal.

Why?

1 Answers

It seems that there is a wording defect. Either your code is intended to be legal, or it's not. If it is intended to be legal, then [basic.life]/6.2 should have an exception for a trivial destructor. If it's not intended to be legal, then the words "with a non-trivial destructor" should be struck from [basic.life]/6.1 because even if the destructor is trivial, the inevitable destructor call will violate p6.2.

I suspect that it was intended to be legal, because there's no point in preserving the explicit exception in the case of delete if not to prevent breaking code that was, in fact, calling delete on objects with trivial destructors that are outside their lifetimes.

N2762 removed the trivial destructor exception for all the other UB-causing operations on pointers to objects outside their lifetimes, but conspicuously kept it for delete-expressions. This suggests that there was a need to keep this exception in the language in order to avoid breaking code.

Most likely the reason why the second bullet was not also changed to make an exception for a trivial destructor call is that the authors forgot that a delete-expression does call the destructor even if it's trivial. They might have assumed that delete was specified to skip the destructor call if it would be trivial.

(Note also that in C++98 through C++17, your S object's lifetime is not ended by the destructor call anyway because [basic.life]/1 specified that a destructor call ends the lifetime of the object only if it is non-trivial; this was changed in C++20 by CWG2256. However, due to N2762, in C++11 and later, if an object's lifetime has not yet started because it has nontrivial initialization, we can run into an issue that is similar to the one described by the OP.)

I think it's worth filing a defect report about this, though I'm not sure what the outcome will be. So much time has elapsed since N2762 that I would imagine the committee would actually want to consider whether the exception for trivial destructors ought to be eliminated at this point (i.e., fixing p6.1 instead of p6.2); after all, the resolution of CWG2256 may also have broken some code, but it was probably code of the sort that has become increasingly rare over the years since C++ diverged from C.

Related