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

1

6

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.

Firewater answered 7/9, 2023 at 10:43 Comment(7)
I wouldn't say that placement new is an implicit creation. It's rather explicit. It's the std::malloc whch does implicit creation. You should save the pointer it returns and use thatRecalescence
@Swift-FridayPie I never said that placement-new creates anything implictly. In this case, it's the creation of std::byte[sizeof(buffer)] array that creates objects implicitly. No need to save any pointer to it. Besides, I don't see how that aligns with the question. Maybe the title is a bit misleading, thoughFirewater
ending the lifetime of the int in question? Yes, this is correct. effectively getting rid of the value that we wished to pun? Objects created by new-expression (and their subobjects) have dynamic storage duration, timsong-cpp.github.io/cppwp/n4868/basic.indet#1 apply to std::byte array elementsNotation
@LanguageLawyer so that means that new (buffer) std::byte[sizeof(buffer)] is guaranteed to preserve the value that the (now dead) int had?Firewater
How does timsong-cpp.github.io/cppwp/n4868/basic.indet#1 guarantee this?Notation
I don't see how it does so, but I got that impression from the second part of your comment. What is the purpose of the section you linked, then?Firewater
Objects created by new-expression (and their subobjects) have dynamic storage. Per timsong-cpp.github.io/cppwp/n4868/basic.indet#1, objects with automatic and dynamic storage duration have indeterminate value until initialized (if there is initialization). Sounds as opposite to preserving the value, doesn't it?Notation
I
1

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.

Immediate answered 7/9, 2023 at 14:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.