'changing active constexpr union member using construct_at

I'm trying to change the active member of a constexpr union using construct_at and get the following error when constructor initializes it's member using initializer list vs. member. Can someone explain why?

#include <memory>
struct Z { 
 #if 1  // If this changes to zero it does not compile   
   constexpr Z(int x) : y(x){      
   }  
  #else  
     constexpr Z(int x) {
         y = x;      
   }  
  #endif
   int y;
};

struct W { 
   constexpr W(int x) {
      y = x;
   }   
   W(const W&) {}
   int y;
};

union U { 
   Z z;
   W w;
   constexpr U(int z) : w(z) {
   }   
};

constexpr int func() {
   constexpr U u(10);
   std::construct_at(&u.z, 10);
//   ::new (&u.z) Z(10);
   return u.z.y;
}

int main() {
    static_assert(func() == 1);
}

Error:

source>: In function 'int main()':
<source>:37:26: error: non-constant condition for static assertion
   37 |     static_assert(func() == 10);
      |                   ~~~~~~~^~~~~
<source>:37:23:   in 'constexpr' expansion of 'func()'
<source>:31:21:   in 'constexpr' expansion of 'std::construct_at<const Z, int>((& u.U::z), 10)'
/opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/stl_construct.h:97:14:   in 'constexpr' expansion of '((Z*)<anonymous>)->Z::Z(<anonymous>)'
<source>:8:12: error: modifying a const object '((Z*)this)->Z::y' is not allowed in a constant expression
    8 |          y = x;
      |          ~~^~~
<source>:30:16: note: originally declared 'const' here
   30 |    constexpr U u(10);
      |                ^
Compiler returned: 1


Solution 1:[1]

While I don't know what exactly the case with working/non-working ctors, you have a constexpr U u(10); on which you later try to call a modifying function (ctor) by calling std::construct_at(&u.z, 10);. What do you expect from trying to modify a constexpr object? Remove constexpr on the u object, it won't make your function less constexpr.

Solution 2:[2]

As noted in the other answer, constexpr on u is wrong here. But I will try to say something about the compiler behavior with your code.

std::construct_at will construct a new object at the given storage location. However constexpr implies const, meaning you are trying to create a new object in a const complete object with automatic storage duration. That is not allowed and causes undefined behavior. (The call to std::construct_at is however not ill-formed. It may be called with a const pointer.)

Since this is happening during the evaluation of an expression that is required to be a constant expression, the question is then whether this makes the expression not a constant expression, i.e. whether the compiler has to diagnose the undefined behavior.

Generally any core language undefined behavior must be diagnosed by the compiler. Any standard library undefined behavior may be diagnosed by the compiler.

So the question would be here as which the std::construct_at call counts. The function itself is specified as equivalent to the corresponding placement-new ([specialized.construct]/2), except that a special exception is made that std::construct_at is allowed in a constant expression unless "the underlying constructor call disqualifies [the expression] from being a core constant expression" ([expr.const]/6.1).

Although I don't think it is really clear, I would guess that this is supposed to count as core language undefined behavior since it is not violating any precondition or stated undefined behavior in the library clauses, but rather one stated in the core language clauses.

With that the expression func() == 1 would not be a constant expression in either of the two versions of the code and the compiler should put out a diagnostic such as GCC is doing in at least one of the shown cases.

However, as I hinted above, I am not confident that my interpretation of the constant expression requirements is the intended one here.

I guess [expr.const]/6.1 could also be read such that the undefined behavior requirement does not apply to a std::construct_at call at all, but only to the underlying constructor call. But that does really seem unintended.

Solution 3:[3]

I was wrong, this is not related.

As it is mentioned in the comments, it works with recent versions of clang. Initially assignment in constexpr constructors weren't allowed in C++11 (check $4 in section 7.1.5), but were allowed in C++14 (same section) in my opinion by N3652. Check the related changes for (7.1.5)/3 and (7.1.5)/4. So if you are building with C++14 with GCC, then you are struck by a compiler bug.

This answer is heavily based on another SO answer.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ixSci
Solution 2
Solution 3