reinterpret_cast creating a trivially default-constructible object
Solution 1:
There is no X
object, living or otherwise, so pretending that there is one results in undefined behavior.
[intro.object]/1 spells out exhaustively when objects are created:
An object is created by a definition ([basic.def]), by a new-expression ([expr.new]), when implicitly changing the active member of a union ([class.union]), or when a temporary object is created ([conv.rval], [class.temporary]).
With the adoption of P0137R1, this paragraph is the definition of the term "object".
Is there a definition of an X
object? No. Is there a new-expression? No. Is there a union? No. Is there a language construct in your code that creates a temporary X
object? No.
Whatever [basic.life] says about the lifetime of an object with vacuous initialization is irrelevant. For that to apply, you have to have an object in the first place. You don't.
C++11 has roughly the same paragraph, but doesn't use it as the definition of "object". Nonetheless, the interpretation is the same. The alternative interpretation - treating [basic.life] as creating an object as soon as suitable storage is obtained - means that you are creating Schrödinger's objects*, which contradicts N3337 [intro.object]/6:
Two objects that are not bit-fields may have the same address if one is a subobject of the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they shall have distinct addresses.
* Storage with the proper alignment and size for a type T
is by definition storage with the proper alignment and size for every other type whose size and alignment requirements are equal to or less than those of T
. Thus, that interpretation means that obtaining the storage simultaneously creates an infinite set of objects with different types in said storage, all having the same address.
Solution 2:
Based on p0593r6 I believe the code in the OP is valid and should be well defined. The new wording, based on the DR retroactively applied to all versions from C++98 inclusive, allows implicitly object creation as long as the created object is well defined (tautology is sometimes the rescue for complicated definitions), see § 6.7.2.11 Object model [intro.object]):
implicitly-created objects whose address is the address of the start of the region of storage, and produce a pointer value that points to that object, if that value would result in the program having defined behavior [...]
See also: https://stackoverflow.com/a/61999151/2085626
Solution 3:
This analysis is based on n4567, and uses section numbers from it.
§5.2.10/7: When a prvalue v
of object pointer type is converted to the object pointer type “pointer to cv T”, the result is static_cast<cv T*>(static_cast<cv void*>(v))
.
So, in this case, the reinterpret_cast<X*>(buffer)
is the same as static_cast<X *>(static_cast<void *>(buffer))
. That leads us to look at the relevant parts about static_cast
:
§5.2.9/13: A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. The null pointer value is converted to the null pointer value of the destination type. If the original pointer value represents the address A
of a byte in memory and A
satisfies the alignment requirement of T
, then the resulting pointer value represents the same address as the original pointer value, that is, A
.
I believe that's enough to say that the original quote is sort of correct--this conversion gives defined results.
As to lifetime, it depends on what lifetime you're talking about. The cast creates a new object of pointer type--a temporary, which has a lifetime starting from the line where the cast is located, and ending whenever it goes out of scope. If you have two different conversions that happen conditionally, each pointer has a lifetime that starts from the location of the cast that created it.
Neither of these affects the lifetime of the object providing the underlying storage, which is still buffer
, and has exactly the same lifetime, regardless of whether you create a pointer (of the same or converted type) to that storage or not.