Reputation: 303447
cppreference† states that:
Objects with trivial default constructors can be created by using
reinterpret_cast
on any suitably aligned storage, e.g. on memory allocated withstd::malloc
.
This implies that the following is well-defined code:
struct X { int x; };
alignas(X) char buffer[sizeof(X)]; // (A)
reinterpret_cast<X*>(buffer)->x = 42; // (B)
Three questions follow:
X
begin? If on line (B)
, is it the cast itself that is considered acquiring storage? If on line (A)
, what if there were a branch between (A)
and (B)
that would conditionally construct an X
or some other pod, Y
?†Note that this is an old link. The wording was changed in response to this question. It now reads:
Unlike in C, however, objects with trivial default constructors cannot be created by simply reinterpreting suitably aligned storage, such as memory allocated with
std::malloc
: placement-new is required to formally introduce a new object and avoid potential undefined behavior.
Upvotes: 59
Views: 3286
Reputation: 13810
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
Upvotes: 8
Reputation: 137425
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.
Upvotes: 39
Reputation: 490408
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.
Upvotes: 4