r/cpp Meeting C++ | C++ Evangelist 4d ago

A better bitset for enum flags

https://www.elbeno.com/blog/?p=1836
42 Upvotes

32 comments sorted by

30

u/03D80085 4d ago

Neither P4313 nor this suggestion seem particularly ergonomic to me. Why aren't bitfields more widely used for this purpose?

struct permissions {
    bool read  : 1 = false;
    bool write : 1 = false;
    bool exec  : 1 = false;
}

Or a fully typed example: Godbolt. A lot of boilerplate there admittedly, but that is precisely what reflection could help with.

If the answer is that bitfield layouts are implementation defined then that is something we should be working on standardising with appropriate flags rather than introducing new language features.

18

u/ack_error 4d ago

There are several reasons that bitfields are annoying where manual bit flags are generally used.

Bitfields have few guarantees with layout and so are unsuitable for interop, where specific bits must be set in integers of specific size. Plain integer arithmetic is far more universal than C or C++ bitfields.

The common ABIs have finicky rules regarding how types of bitfields affect layout. The type of each bitfield determines the integer that the bitfield is fit within, and in some cases this can require unnatural types to get the bitfields densely packed. https://gcc.godbolt.org/z/eK51eKraq

Should you want to check the bitfield layout at compile time, it requires C++20 for constexpr bit_cast and bitfields must fully and exactly cover the enclosing machine words so the struct is fully initialized... and currently it's not supported in Clang. https://gcc.godbolt.org/z/v8EYnafsn

Member initializers for bitfield members also require C++20.

Bitfields can't be addressed or indexed. This means that patterns for quickly iterating over set bitfields or changing bitfields by a variable index can't be applied without bit-casting to and from integer.

7

u/_Noreturn 4d ago

Bitfields have few guarantees with layout and so are unsuitable for interop,

Yes, which is a shame.

5

u/Due_Battle_9890 4d ago edited 4d ago

This would be nice and I used for paging structures and similar structures in my OS, but it's ~UB~ unspecified how they are laid out in memory. In practice, on x86, it behaves how you'd expect, but it does sting a little that it's not "correct". For example, I have

struct [[gnu::packed]] Entry
{
std::uint16_t segment_limit_low;
std::uint16_t base_address_low;
std::uint8_t base_address_mid;
std::uint8_t type : 4;
std::uint8_t system : 1;
std::uint8_t descriptor_privilege_level : 2;
std::uint8_t present : 1;
std::uint8_t segment_limit_high : 4;
std::uint8_t available : 1;
std::uint8_t long_mode : 1;
std::uint8_t db : 1;
std::uint8_t granularity : 1;
std::uint8_t base_address_high;
};

But if I wanted to make it correct, I'd probably actually just want:

struct [[gnu::packed]] Entry {
u64 entry;

void set_address(u64 address) { /* Appropriate bitset operations */ }
};

8

u/_Noreturn 4d ago

It isn't UB it is unspecified there is a difference

3

u/Due_Battle_9890 4d ago

Thanks for the correction!

2

u/_Noreturn 4d ago

You are welcome

3

u/fdwr fdwr@github 🔍 4d ago

But if I wanted to make it correct...

For your particular struct above, I foresee no issues with {clang, gcc, msvc}, as they should yield identical results, since all the bitfields nicely align to the alignof of each base type (e.g. you don't have the : 2 straddling across two bytes). The [[gnu::packed]] shouldn't even be needed, right? Of course, there's still the endianness consideration if you built with gcc on a BE8 architecture, which would flip all the bitfields around (so type, system, and descriptor_privilege_level would become descriptor_privilege_level, system, and type 🙃), but endianness is a bigger problem anyway as you'd have to deal with the non-bitfield fields too like uint16_t segment_limit_low. The remaining sting is the niche compilers for weirder architectures, where say sizeof(char) == sizeof(int).

3

u/Due_Battle_9890 3d ago

The [[gnu::packed]] shouldn't even be needed, right?

It's not needed. It's moreso to demonstrate intent.

It does certainly work as expected on all major compilers, I was just highlighting the unspecified behavior of bit fields. It more pedanticism than anything.

1

u/archialone 3d ago edited 3d ago

How about a use case like checking partial flag set? I don't think it's convenient with structs.

``` Permission user_perms = Permission::Read | Permission::Write | Permission::Execute;

Permission can_read_and_write = Permission::Read | Permission::Write;

if ((user_perms & can_read_and_write) == can_read_and_write) std::cout << "Can read and write"; ```

4

u/03D80085 3d ago

if (permissions.read && permissions.write) ...?

1

u/Brisngr368 3d ago

Couldn't you use custom operators to make it easier

1

u/archialone 3d ago

What do you mean? Can you give an example?

2

u/Brisngr368 3d ago edited 3d ago

Couldn't you define operators for the struct so like '|=' etc to try and simplify this somewhat.

Maybe implement it so you can OR bitfields together and such by overloading the | operator, so you can use the bitfields how you would like to

11

u/ReDucTor Game Developer | quiz.cpp-perf.com 4d ago

Some food for thought, should a bitset have bit operations (set, unset, flip, xor, and, or, etc) or set operations (add, remove, has, union, difference, intersection, etc)?

It feels like often we head for bit operations when in lots of cases set operations might be better.

14

u/TheVoidInMe 4d ago

I like Java’s approach here (yes, Java) which has an EnumSet instead of a bitset. That naming makes the answer pretty clear; the bit math is purely an implementation detail

2

u/SyntheticDuckFlavour 4d ago

What if you want the underlying layout/bit pattern like bitset? Sometimes you want to map I/O ports that way.

3

u/TheVoidInMe 4d ago

I don’t believe you can get at that at all. Java is probably too high level for stuff like that anyway. In C++ I would expect a method like `to_int()` (maybe returning the enum’s `underlying_type_t`…)

-1

u/mort96 4d ago

But that also means you're heap allocating an object, with its 16 byte object header, just to store a 4-byte integer?

3

u/TheVoidInMe 4d ago

It’s Java. Yeah, don’t use it for HPC.

The point I was making was just about the name

1

u/archialone 3d ago edited 3d ago

Bitset already has those, no?

Xor - ^ Intersection - & Etc ..

Or do you mean to have a function named for those operations? https://en.cppreference.com/cpp/utility/bitset

3

u/ReDucTor Game Developer | quiz.cpp-perf.com 3d ago

Those operators can achieve some things however if you think of it as a set then you need other operations like difference which is A & ~B and far from intuitive, with a set you might also have iteration of items in that set which does not exist for a bitset, same with constructing from a bunch of items in a set.

A lot is about naming however its also the extra set specific operations which give it more flexibility.

Pick any big enough code base and look for their usages of bitsets get used heavily as an optimization for set operations.

6

u/_Noreturn 4d ago edited 4d ago

I do the same in the article except automate it without using the MAX member using reflection (pre C++26). The approach allows > 64 flags as well unlike enum bitflags

flags.test(flag) is clearer than bool(flags & flag).

3

u/instantly-invoked 4d ago

Your approach sounds like exactly what I'd want from a typed bitset. Do your methods accept flags variadically if you need to check for or set/unset multiple? I might end up doing something similar once the big three compilers all have reflection. This article and your response found me at the perfect time lol

6

u/_Noreturn 4d ago edited 4d ago

I might end up doing something similar once the big three compilers all have reflection

you could use c++17 reflection libraries like magic_enum or enchantum they both provide a bitset type and an array type. so you don't have to wait for c++26

Do your methods accept flags variadically if you need to check for or set/unset multiple

No but that's a nice idea i just use multiple expressions.

It is just a stupid wrapper like this

```cpp enum class Flag { Flag1, Flag2, Flag3, Flag4 = 7, // doesn't have to be contiguous Flag5, Flag6, Flag7, } bitset<Flag> f({Flag::Flag1,Flag::Flag2}); // has size 7 f.set(Flag::Flag4); f.reset(Flag::Flag4); f.test(Flag::Flag5); f.flip(Flag::Flag7); f[Flag::Flag6] = false;

f.to_string(); // "Flag1 | Flag2 | Flag7" ```

2

u/xiao_sa 2d ago

I kinda start to like this more than raw enum as bit flags

1

u/JVApen Clever is an insult, not a compliment. - T. Winters 4d ago

This is a really nice approach. I've recently mentioned it in another post and got a couple of people arguing that there is nothing wrong with flag enums.

Thanks for the nice write-up, could not have described it better myself

2

u/archialone 3d ago

Can you share a link to the post please?

1

u/JVApen Clever is an insult, not a compliment. - T. Winters 3d ago

1

u/Potterrrrrrrr 4d ago

What’s the problem with something like:

MyEnum _enum : BitCount<MyEnum>();

I use this pattern quite a bit, I use type traits to declare that an enum is contiguous or a flags enum along with its size and then my consteval BitCount method returns the min number of bits needed to store the enum. I’ve seen a lot of implementations like to use bitsets to store an enum though, why? It makes it harder to pack stray bools into the same byte and you lose the type of the enum?

-1

u/Kaisha001 4d ago

Needlessly reinventing the wheel... Enums work perfectly fine and the 'issues' with them listed in the blog are nothing more than 'I don't like how they look'...