Look at this example (godbolt):
#include <memory>
union U {
int i[1];
};
constexpr int foo() {
U u;
std::construct_at(u.i, 1);
return u.i[0];
}
constexpr int f = foo();
gcc and msvc successfully compile this, but clang complains:
construction of subobject of member 'i' of union with no active member is not allowed in a constant expression
Which compiler is right? I think that clang is wrong here, because C 20's implicit creation of objects (P0593) should make this program valid (because the array should be implicitly created, which should make u.i
active), but I'm not sure.
CodePudding user response:
U u;
does not begin the lifetime of the i
subobject. Beginning the lifetime of a variable other than an array of type char
, unsigned char
or std::byte
is also not one of the operations specifically qualified to be implicitly creating objects. [basic.intro.object]/13
Therefore at this point the i
member is definitively not active and no array object exists.
As mentioned by @Sebastian in the question comments, calling std::construct_at
on u.i
is then not allowed in a constant expression since [expr.const]/6.1 specifically requires the provided pointer to point to an object whose lifetime began during the evaluation of the constant expression (or be storage returned from std::allocator
).
Therefore Clang seems correct to me. There is an open GCC bug for exactly this issue here.
I am not sure that this is the intended interpretation though, since Clang does accept the program if a non-array type is used for the member, which by my reasoning would equally not be allowed.
The relevant wording is a consequence of this comment.
Now, suppose you replace the std::construct_at
call with
u.i[0] = 1;
Then this assignment will begin the lifetime of the array object, as described in [class.union.general]/6. This is not disqualified for constant expressions since C 20 either. Therefore the code will not be ill-formed with this.
Whether the std::construct_at
version has defined behavior if used outside a constant expression context, I am not entirely sure.
But I think that std::construct_at
being specified to be equivalent to a new-expression means that it will call operator new
, which is specified to implicitly create objects in the storage it returns. [basic.intro.object]/13
Whether operator new
must be an allocating operator new
call for this to be true is not fully clear to me. I think the wording "in the returned region of storage" does not require it.
i
is of type int[1]
, which is an implicit-lifetime type, which are implicitly created if necessary by operations qualified to implicitly create objects. [basic.types.general]/9
Therefore I think that construct_at
will implicitly create the array object i
and begin its lifetime. I also think that [basic.intro.object]/2 will guarantee that this object becomes subobject of the union, so that u.i
will refer to it.
However, given that the storage operated on is only the size of a single int
and assuming that this is also the storage meant in [basic.intro.object]/13, only an array of length 1
can be implicitly created in it. Therefore if i
was of length larger than 1
, the implicitly created array could not overlap exactly with the member and can therefore not become subobject of the union.
In this case implicit object creation could not make u.i[0]
defined behavior.
There is a discussion of this issue here which seems to indicate that already forming the pointer to the first element of u.i
outside its lifetime is UB, in which case the construct_at
version with array would more directly have UB, but at least compilers accept both auto x = u.i;
and auto x = &u.i[0];
in a constant expression without complaining. As mentioned in the comments to this answer, this also seems wrong.
CodePudding user response:
Deferred initialization of an array in a constexpr environment can be achieved with std::allocator and std::construct_at():
#include <memory>
constexpr int foo() {
std::allocator<int> alloc;
int* i; // pointer to first element of array
i = alloc.allocate(100); // allocate memory for 100 elements
std::construct_at(&i[0], 1); // initialize first element (first call of constructor)
int r = i[0];
alloc.deallocate(i, 100); // deallocate before leaving
return r;
}
constexpr int f = foo();
Pointers to the relevant standard clauses
allocate:
[utilities.memory.default.allocator.members]/5: std::allocator<>::allocate() obtains storage by calling operator ::new and starts the lifetime of the array object, but not the lifetime of the array elements themselves.
[expr.const]/5.19: Explicitly allows std::allocator<>::allocate() in constant expressions, if the memory is deallocated again within the constant expression
construct_at:
[algorithms.specialized.construct]/2: std::construct_at() effectively calls placement new.
[expr.const]/6: Explicitly allows std::construct_at in constant expressions, if the memory is allocated by std::allocator
deallocate:
[expr.const]/5.19: Explicitly allows std::allocator<>::deallocate() in constant expressions, if the memory was allocated before within the constant expression