Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does std::construct_at make an array member of a union active?

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.

like image 999
geza Avatar asked Nov 17 '25 06:11

geza


1 Answers

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 the array object is not alive.

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.

In any case, it is not intended that implicit object creation happens in constant expressions although it currently seems to (question), see CWG issue 2469.

Without implicit object creation as explained below, the use in a context requiring a constant expression should then be ill-formed independently of the std::construct_at restriction and the following considerations.


Whether the construction 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 an array object at u.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 return 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.


All in all I think that std::construct_at can generally not be used to activate an array member of a union.


But, 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 if used in a context requiring a constant expression, nor will it have undefined behavior outside of that.

like image 81
user17732522 Avatar answered Nov 19 '25 21:11

user17732522