Why isn't it undefined behaviour to destroy an object that was overwritten by placement new?

Viewed 413

I'm trying to figure out whether the following is undefined behaviour. I have a feeling it's not UB, but my reading of the standard makes it look like it is UB:

#include <iostream>

struct A {
    A() { std::cout << "1"; }
    ~A() { std::cout << "2"; }
};

int main() {
    A a;
    new (&a) A;
}

Quoting the C++11 standard:

basic.life¶4 says "A program may end the lifetime of any object by reusing the storage which the object occupies"

So after new (&a) A, the original A object has ended its lifetime.

class.dtor¶11.3 says that "Destructors are invoked implicitly for constructed objects with automatic storage duration ([basic.stc.auto]) when the block in which an object is created exits ([stmt.dcl])"

So the destructor for the original A object is invoked implicitly when main exits.

class.dtor¶15 says "the behavior is undefined if the destructor is invoked for an object whose lifetime has ended ([basic.life])."

So this is undefined behaviour, since the original A no longer exists (even if the new a now exists in the same storage).

The question is whether the destructor for the original A is called, or whether the destructor for the object currently named a is called.

I am aware of basic.life¶7, which says that the name a refers to the new object after the placement new. But class.dtor¶11.3 explicitly says that it's the destructor of the object which exits scope which is called, not the destructor of the object referred to by a name that exits scope.

Am I misreading the standard, or is this actually undefined behaviour?

Edit: Several people have told me not to do this. To clarify, I'm definitely not planning on doing this in production code! This is for a CppQuiz question, which is about corner cases rather than best practices.

4 Answers

You're misreading it.

"Destructors are invoked implicitly for constructed objects" … meaning those that exist and their existence has gone as far as complete construction. Although arguably not entirely spelled out, the original A does not meet this criterion as it is no longer "constructed": it does not exist at all! Only the new/replacement object is automatically destructed, then, at the end of main, as you'd expect.

Otherwise, this form of placement new would be pretty dangerous and of debatable value in the language. However, it's worth pointing out that re-using an actual A in this manner is a bit strange and unusual, if for no other reason than it leads to just this sort of question. Typically you'd placement-new into some bland buffer (like a char[N] or some aligned storage) and then later invoke the destructor yourself too.

Something resembling your example may actually be found at basic.life¶8 — it's UB, but only because someone constructed a T on top of an B; the wording suggests pretty clearly that this is the only problem with the code.

But here's the clincher:

The properties ascribed to objects throughout this International Standard apply for a given object only during its lifetime. [..] [basic.life¶3]

Am I misreading the standard, or is this actually undefined behaviour?

None of those. The standard is not unclear but it could be clearer. The intent though is that the new object's destructor is called, as implied in [basic.life]p9.

[class.dtor]p12 isn't very accurate. I asked Core about it and Mike Miller (a very senior member) said:

I wouldn't say that it's a contradiction [[class.dtor]p12 vs [basic.life]p9], but clarification is certainly needed. The destructor description was written slightly naively, without taking into consideration that the original object occupying a bit of automatic storage might have been replaced by a different object occupying that same bit of automatic storage, but the intent was that if a constructor was invoked on that bit of automatic storage to create an object therein - i.e., if control flowed through that declaration - then the destructor will be invoked for the object presumed to occupy that bit of automatic storage when the block is exited - even it it's not the "same" object that was created by the constructor invocation.

I'll update this answer with the CWG issue as soon as it is published. So, your code does not have UB.

Too long for a comment.

Lightness' answer is correct and his link is the proper reference.

But let's examine terminology more precisely. There is

  • "Storage duration", concerning memory.
  • "Lifetime", concerning objects.
  • "Scope", concerning names.

For automatic variables all three coincide, which is why we often do not clearly distinguish: A "variable goes out of scope". That is: The name goes out of scope; if it is an object with automatic storage duration, the destructor is called, ending the lifetime of the named object; and finally the memory is released.

In your example only name scope and storage duration coincide — at any point during its existence the name a refers to valid memory — , while object lifetime is split between two distinct objects at the same memory location and with the same name a.

And no, I think you cannot understand "constructed" in 11.3 as "fully constructed and not destroyed" because the dtor will be called again (wrongly) if the object's lifetime was ended prematurely by a preceding explicit destructor call. In fact, that's one of the concerns with the concept of memory re-use: If construction of the new object fails with an exception the scope will be left and a destructor call will be attempted on an incomplete object, or on the old object which was deleted already.

I suppose you can imagine the automatically allocated, typed memory marked with a tag "to be destroyed" which is evaluated when the stack is unwound. The C++ runtime does not really track individual objects or their state beyond this simple concept. Since variable names are basically constant addresses it is convenient to think of "the name going out of scope" triggering the destructor call on the named object of the supposed type supposedly present at that location. If one of these suppositions is wrong all bets are off.

Imagine using placement new to create a struct B to the storage where the A a objects lives. In the end of the scope, the destructor of the struct A will be called (because the variable a of type A goes out of scope), even if an object of type B is in reallty living there right now.

As already cited:

"If a program ends the lifetime of an object of type T with static ([basic.stc.static]), thread ([basic.stc.thread]), or automatic ([basic.stc.auto]) storage duration and if T has a non-trivial destructor,39 the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place;"

So after putting B into the a storage, you need to destroy B and put an A there again, to not violate the rule above. This somehow not apply here directly, because you are putting an A to an A, but it shows the behavior. It shows, that this thinking is wrong:

So the destructor for the original A object is invoked implicitly when main exits.

There is no "original" object any longer. There is just an object currently alive in the storage of a. And thats it. And on the data currently sitting in a, a function is called, namely the destructor of A. Thats what the program compiles to. If it would magically kept track of all "original" objects you would somehow have a dynamic runtime behavior.

Additionally:

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 ([expr.delete]) 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.

Since the destructor of A is not trivial and has side effects, (i think) its undefined behavior. For build in types, this does not apply (hence you can use a char buffer as object buffer without reconstructing the chars back into the buffer after using it) since they have a trivial (no-op) destructor.

Related