c++language-lawyerlifetimeplacement-newc++23

Does placement-new of array of bytes (which implictly creates objects) end the lifetime of the object that previously occupied that storage?


P0593, under the Type punning section, presents this example:

float do_bad_things(int n) {
  alignof(int) alignof(float)
    char buffer[max(sizeof(int), sizeof(float))];
  *(int*)buffer = n;      // #1
  new (buffer) std::byte[sizeof(buffer)];
  return *(float*)buffer; // #2
}

And states that:

The proposed rule would permit an int object to spring into existence to make line #1 valid [...], and would permit a float object to likewise spring into existence to make line #2 valid.

However, these examples still do not have defined behavior under the proposed rule. The reason is a consequence of [basic.life]p4:

The properties ascribed to objects and references throughout this document apply for a given object or reference only during its lifetime.

Specifically, the value held by an object is only stable throughout its lifetime. When the lifetime of the int object in line #1 ends (when its storage is reused by the float object in line #2), its value is gone. Symmetrically, when the float object is created, the object has an indeterminate value ([dcl.init]p12), and therefore any attempt to load its value results in undefined behavior.

emphasis mine

The proposal claims that the problematic part is the (implicit) creation of a float object. But isn't the previous line (new (buffer) std::byte[sizeof(buffer)]) already reusing the storage (by creating a byte array), ending the lifetime of the int in question? To my understanding, placement-new always ends the lifetime of the object that lived in the memory in which a new object is being created.

Also, this comment says that "New expressions do not promise to preserve the bytes in the storage." Would that mean that new (buffer) std::byte[sizeof(buffer)] could theoretically alter the bytes from buffer, effectively getting rid of the value that we wished to pun?

Just to be clear, I am not looking for a way to achieve type punning. Those are just examples that fit the best for me (that I found so far) to understand the underlying mechanisms of nowadays lifetime management.


Solution

  • But isn't the previous line (new (buffer) std::byte[sizeof(buffer)]) already reusing the storage (by creating a byte array), ending the lifetime of the int in question?

    Yes, although that is a hypothetical, because the int object can only exist by implicit object creation if the program would be given defined behavior by that implicit object creation, which it wouldn't.

    Either way, the result is the same: The float object's value, if it were to exist by implicit object creation, would have an indeterminate value until it is initialized/assigned some value and reading the indeterminate value with return *(float*)buffer; would have UB. The value of a previous object in the same storage, whether the char array elements, int nested object or std::byte array elements, would not affect the initial value of a new object, whether float or std::byte, in the same storage.

    So implicit object creation can't save the program from UB.