Trying to use std::bit_cast with a bitfield struct. Why is it not constexpr?

338 Views Asked by At

I'm trying to get a bitmask from a bitfield struct, at compile time. One of the tricks that I tried, which looks promising to me, is using std::bit_cast, because it is supposed to be constexpr.

My test code can be found here: https://godbolt.org/z/er48M63sh

This is the source:

#include <bit>

struct Bitfield {
    int :3;
    int x:3;
};

constexpr int mask() noexcept {
    Bitfield bf{};
    bf.x -= 1;
    return std::bit_cast<int>(bf);
}

int test() {
    int mask1 = mask();
    // constinit static int mask2 = mask();     // Why doesn't this compile?
    return 0;
}

As you can see, it doesn't actually calculate the bitmask at compile time, but at runtime, so for some reason the constexpr trick isn't working. I fail to see why, however, since cppreference doesn't seem to list my case as one that defeats constexpr in std::bit_cast.

Does anybody see what's wrong? Does this thing have a chance of working?

2

There are 2 best solutions below

1
Jan Schultke On BEST ANSWER

The problem is that using std::bit_cast with padding bits is undefined behavior in most cases:

[bit.cast] p2

A bit in the value representation of the result is indeterminate if it does not correspond to a bit in the value representation of from or corresponds to a bit of an object that is not within its lifetime or has an indeterminate value.

Most of the upper (or lower) bits in the int that results from std::bit_cast<int>(bf) are indeterminate, because it they don't correspond to bits in the value representation of the bit-field. The Bitfield class has plenty of padding bits, i.e. which are only in the object representation, not the value representation of Bitfield.

For each bit in the value representation of the result that is indeterminate, the smallest object containing that bit has an indeterminate value; the behavior is undefined unless that object is of unsigned ordinary character type or std​::​byte type. The result does not otherwise contain any indeterminate values.

int is not a std::byte, so it isn't exempt, and producing an int through std::bit_cast which has some indeterminate bits is undefined behavior. You only notice that it's UB in a constant expression because compilers aren't required to diagnose UB outside constant expressions.

Possible Solutions

  • don't use std::bit_cast, and manually serialize instead with bf.first | bf.second << 3, assuming you've given all the bit-field members a name
  • give all bit-field members a name, and make sure they fit exactly into the containing struct
    • to verify, you can use static_assert(std::has_unique_object_representations_v<Bitfield>)
0
sh- On

It turns out that it depends on the compiler whether this works.

MSVC supports it: https://godbolt.org/z/rEcefGc6f

Clang at least admits explicitly in an error message that it doesn't support it yet.

The c++20 standard appears to mandate this construct as working.

It apparently is a case of waiting until compilers have caught up.

Thanks for everyone helping to answer this.

Edit:

Upon further investigation, I conclude that it must have to do with padding bits in the bitfield. There is a gcc bug about it: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=99637

Arguably, gcc is right in rejecting this, because the initialization of padding bits is undefined, which makes my trick depend on undefined behavior, because it depends on zero-initialization of padding bits. It would be better to emit a warning instead of an error, however.

Of course, the compiler could produce a more helpful error message.

But since, in practice, I have yet to come across a compiler that does anything other than zero initialize the padding bits, I would like to see the C++ specification tightened up, thereby making bitfields more useful.

The fix for me is to give all bitfield members a name, even those used for padding, and make sure that no undefined bits are left at the end, which would also count as padding. If that is done, gcc is happy.