struct A
{
int x;
}
A t{};
t.x = 5;
new (&t) A;
// is it always safe to assume that t.x is 5?
assert(t.x == 5);
As far as I know, when a trivial object of class type is created, the compiler can omit the call of explicit or implicit default constructor because no initialization is required. (is that right?)
Then, If placement new is performed on a trivial object whose lifetime has already begun, is it guaranteed to preserve its object/value representation? (If so, I want to know where I can find the specification..)
CodePudding user response:
Well, let's ask some compilers for their opinion. Reading an indeterminate value is UB, which means that if it occurs inside a constant expression, it must be diagnosed. We can't directly use placement new
in a constant expression, but we can use std::construct_at
(which has a typed interface). I also modified the class A
slightly so that value-initialization does the same thing as default-initialization:
#include <memory>
struct A
{
int x;
constexpr A() {}
};
constexpr int foo() {
A t;
t.x = 5;
std::construct_at(&t);
return t.x;
}
static_assert(foo() == 5);
As you can see on Godbolt, Clang, ICC, and MSVC all reject the code, saying that foo()
is not a constant expression. Clang and MSVC additionally indicate that they have a problem with the read of t.x
, which they consider to be a read of an uninitialized value.
P0593, while not directly related to this issue, contains an explanation that seems relevant:
The properties ascribed to objects and references throughout this document apply for a given object or reference only during its lifetime.
That is, reusing the storage occupied by an object in order to create a new object always destroys whatever value was held by the old object, because an object's value dies with its lifetime. Now, objects of type A
are transparently replaceable by other objects of type A
, so it is permitted to continue to use the name t
even after its storage has been reused. That does not imply that the new t
holds the value that the old t
does. It only means that t
is not a dangling reference to the old object.
Going off what is said in P0593, GCC is wrong and the other compilers are right. In constant expression evaluation, this kind of code is required to be diagnosed. Otherwise, it's just UB.
CodePudding user response:
This answer is wrong - leaving it up temporarily to keep the comments visible.
I think you are correct. (Technically, at least. I would avoid relying on this, myself.)
All paragraph numbers refer to and hyperlinks go to a C Standard draft N4861, close to the official C 20.
- If the new-initializer is omitted, the object is default-initialized [dcl.init].
To default-initialize an object of type
T
means:
- If
T
is a (possibly cv-qualified) class type, ...- If
T
is an array type, ...- Otherwise, no initialization is performed.
Since A
is an aggregate class and no initializer is provided, default-initializing the replacement A
object will default-initialize its int
member, which performs no initialization.
It's worth noting that the original A
object and the A
object created by the placement new are two distinct objects, and the lifetime of the first ends when the lifetime of the second begins. Therefore the same is true of the two int
objects. However, this is covered by [basic.life]/8:
If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if the original object is transparently replaceable (see below) by the new object. An object o1 is transparently replaceable by an object o2 if:
- the storage that o2 occupies exactly overlays the storage that o1 occupied, and
- o1 and o2 are of the same type (ignoring the top-level cv-qualifiers), and
- o1 is not a complete const object, and
- neither o1 nor o2 is a potentially-overlapping subobject, and
- either o1 and o2 are both complete objects, or o1 and o2 are direct subobjects of p1 and p2, respectively, and p1 is transparently replaceable by p2.
So inside the last assert
, t
can be used to name the second A
object created by placement new, and t.x
names its int
subobject.
It also can't be the case that the int
subobject created by the placement new has an indeterminate value, which is defined in [basic.indet]/1:
When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced ([expr.ass]).
An indeterminate value is associated with obtaining storage. Here the storage was obtained for the first A
object. The second int
object in fact had its indeterminate value replaced before its lifetime began.