Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 847
knatten Avatar asked Sep 03 '18 16:09

knatten


People also ask

Which method is automatically called if it exists when an object is destroyed?

Destructor is an instance member function which is invoked automatically whenever an object is going to be destroyed. Meaning, a destructor is the last function that is going to be called before an object is destroyed. Destructor is also a special member function like constructor.

Is a special function that is used to destroy a class object when the object of the class goes out of scope?

A destructor is called for a class object when that object passes out of scope or is explicitly deleted. A destructor is a member function with the same name as its class prefixed by a ~ (tilde).

In what order are objects destroyed?

Static objects are destroyed in the reverse of the order in which they're constructed (e.g. the first-constructed object is destroyed last), and you can control the sequence in which static objects are constructed, by using the technique described in Item 47, "Ensure that global objects are initialized before they're ...

Is called to destroy the object?

- Destructors are called automatically to destroy the object. There's only one destructor for each object. The name of the destructor is the name of the class, preceeded by a tilde (~).


3 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]

like image 194
Lightness Races in Orbit Avatar answered Sep 29 '22 16:09

Lightness Races in Orbit


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.

like image 22
Rakete1111 Avatar answered Sep 30 '22 16:09

Rakete1111


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.

like image 29
Peter - Reinstate Monica Avatar answered Sep 26 '22 16:09

Peter - Reinstate Monica