3

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?

sh-
  • 941
  • 6
  • 13
  • 1
    `std::bit_cast` is a cast that will also start the lifetime of the object type it is being cast too and is used to change "raw memory" into a valid object instance. It has nothing to do with bitfields. Also be very careful with bitfields they probably do not do what you want them to do either. They will only make the member behave as if it had *n* number of bits, but they will take up more bits in memory, and the actual implementation is different from one compiler to another (allowed by the C++ standard). If you want full control of your bits use masking/shifting. – Pepijn Kramer Aug 18 '23 at 19:57
  • @Pepijn Kramer: I am aware of the limitations of bitfields, by and large. Retrieving the mask in the manner shown actually permits me to investigate how the compiler allocates the bits, and if it worked, it would be portable. So don't worry about the bitfield limitations. I'm interested in why this trick isn't working. – sh- Aug 18 '23 at 20:02
  • 2
    Please make your question self-contained. Code on external sites can change or disappear, making your question practically worthless. Remember that this site isn't only for you to get help right here and now, but also for others that might have the same or a similar problem in the future. Posting links to external sites is okay, but it shouldn't be the only way to get to crucial information. – Some programmer dude Aug 18 '23 at 20:02
  • 2
    clang gives a better error message: `constexpr bit_cast involving bit-field is not yet supported` – NathanOliver Aug 18 '23 at 20:04
  • So it should work, and the implementations just haven't caught up yet? Should it work in C++20? – sh- Aug 18 '23 at 20:05
  • *Does this thing have a chance of working?* Yes, I expect compiler vendors will finish making compilers compliant with that version of the standard soon (3-6 years from now). – Eljay Aug 18 '23 at 20:22
  • I think the behavior of std::bit_cast is missing in the C++ standard (https://eel.is/c++draft/bit.cast). A lot of the standard has exceptions described for bitfields and I see none here. And since bitfields are implementation defined describing the behavior might be tricky since bit_cast at least describes a 1-1 memory layout. – Pepijn Kramer Aug 18 '23 at 20:22
  • Sorry, but do you expect `std::bit_cast(bf)` to actually do anything useful? You know that the mapping of bitfields to actual bits is implementation-defined, right? – Paul Sanders Aug 18 '23 at 20:22
  • It is useful precisely because the bitfield implementation is implementation dependent. It should allow retrieving a mask that matches the bitfield allocation used by the compiler. – sh- Aug 18 '23 at 20:46
  • Ah, I see. Fair enough. – Paul Sanders Aug 18 '23 at 20:55
  • 1
    msvc [compiles](https://godbolt.org/z/rEcefGc6f) it, for a change. – n. m. could be an AI Aug 18 '23 at 20:57
  • That's what I would have expected! Good find! Pity I need to use gcc. But this gives me hope that it really is supposed to work that way, and it is just the compilers that need to catch up. Thanks! – sh- Aug 18 '23 at 21:04
  • 1
    With explicit named padding bits, gcc accepts it too [Demo](https://godbolt.org/z/55cMWPv1j). – Jarod42 Aug 18 '23 at 21:15
  • Gee, that must count as a gcc bug! – sh- Aug 18 '23 at 21:32

2 Answers2

3

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>)
Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • 1
    That's basically what I found, but explained a lot better. Thank you! Upvoted. – sh- Aug 19 '23 at 09:27
1

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.

sh-
  • 941
  • 6
  • 13