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

Time:01-02

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

  • Related