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 afloat
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 thefloat
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.
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.