Can you implicitly activate array member of union in a C++20 constexpr constructor?

130 views Asked by At

I'm using C++20 and have a structure that contains a transparent union that contains an array. I need my structure to have a constexpr constructor, but I would like to avoid filling the entire array in the event that the constructor is not called in a constexpr context. Gcc allows me to do this, while clang does not.

Here is a minimal example:

#include <algorithm>
#include <type_traits>

struct test {
  union {
    char buf_[15];
  };

  // Both gcc and clang accept:
  // constexpr test() : buf_{} { fillbuf(); }

  // Gcc accepts and clang rejects:
  constexpr test() { fillbuf(); }

  // Clang accepts and gcc rejects
  // constexpr test() {}

  constexpr void fillbuf() {
    if (std::is_constant_evaluated())
      std::fill_n(buf_, sizeof(buf_), 0);
  }
};

constinit test mytest{};

With the second constructor (the one I want), clang complains that my constructor is not constexpr because "assignment to member 'buf_' of union with no active member is not allowed in a constant expression."

Note that with the third constructor (commented out above), clang accepts the code while gcc (sensibly, I think) complains that "'test()' is not a constant expression because it refers to an incompletely initialized variable."

Which compiler is right? Is there an alternative way to achieve what I need? In many cases I only need the first byte of the buffer, so don't want the overhead of filling the whole thing when constructing an object in a non-constexpr context.

For what it's worth, I'm using gcc 13.2.1 and clang 16.0.6.

2

There are 2 answers

2
user17732522 On BEST ANSWER

Yes, you can activate a trivial array union member in a constant expression just as normally at runtime with C++20.

The problem is that

std::fill_n(buf_, sizeof(buf_), 0);

can't activate any member. The only form of expression that can implicitly start the lifetime of objects like this is a built-in or trivial assignment expression whose left-hand is a (chain of) built-in member access and array index operations on an expression that nominates a union member. See [class.union.general]/6 for the details.

Because fill_n will however assign through a pointer to the union member, i.e. without naming the union member, it does not qualify for that special rule. Therefore it can't start the lifetime of a union member and can't make a union member active, whether at runtime or in a constant expression. The fill_n call has undefined behavior at runtime.

So, you can easily make the member active before the call to fill_n yourself by adding:

buf_[0] = 0 /* or anything else */;

or directly using a loop to assign to buf_[i] (not through a reference or pointer).

Clang follows this rule strictly, while GCC is being overly permissive.

0
Artyer On

[expr.basic.lval]p11:

If a program attempts to access the stored value of an object through a glvalue whose type is not similar to one of the following types, the behavior is undefined:

  • the dynamic type of the object,
  • a type that is the signed or unsigned type corresponding to the dynamic type of the object, or
  • a char, unsigned char, or std::byte type.

So, std::fill_n(buf_, sizeof(buf_), 0); should set the object representation of *this to all zeroes without making buf_ the active union member.