A better bitset for enum flags

jandeboevrie1 pts0 comments

A better bitset for enum flags – Why is a raven like a writing desk?

Skip to content

Why is a raven like a writing desk?<br>Thoughts both confusing and enlightening.

TL;DR: using enum for bitflags is poor, but it’s what we’re going to end up with.

Many electrons have been spent expounding enumeration bitflag types (and not just in C++). As a tech community across multiple languages, we seem to be all in on the idea that the right way for this to look is something like:

// I somehow opt in for bitwise operations on this type<br>enum struct permissions {<br>NONE = 0,<br>READ = 1

Where in C++, opting in variously looks like adding a function declaration found by ADL, or adding a reflection annotation, or using a macro to provide operators, or…

We might well end up here in C++. With successive standards, we get new techniques which enable us to get there a little more easily, and C++26 reflection is the latest example of that. P4313 is a proposal for enum flags in this style.

I think it’s a workable approach (i.e. we will make it work), but ultimately it seems to me the wrong direction. I think using a bitset is a much better solution. Unfortunately, std::bitset can’t get us there, which is why we may end up with the enum bitflags.

The pros of enum bitflags are usually given as:

Type safety (at least when using scoped enumerations). There is no implicit conversion between values of different types. We can’t accidentally confuse permissions with some other value.

Ergonomics of use. Using these things looks like using plain old integers. (I note that P4313 says "plain C-style int", notwithstanding clang-tidy’s bugprone-signed-bitwise check.)

I think the downsides are:

Ergonomics of implementation. As I said, this is lessened with each C++ standard. And this is something the culture of C++ library authorship discounts: who cares how difficult it is to implement under the hood? But it’s still going to be kind of annoying to apply bit operations to enumeration types you don’t own.

Aesthetics. I don’t want using these things to look like using plain old integers, because using plain old integers is fraught with peril. Which leads to…

Ergonomics of use. Bitwise operators famously have the "wrong" precedence in C and C++, for historical reasons. Yes, please use your compiler warnings. Ironically the expositional code used in P4313 exhibits this very operator precedence bug (which I corrected above)!

(Perhaps most significantly…) Conflation of ideas. enum bitflags are easy, but not simple. They complect naming bits (what we actually want) with representing them. True, it’s not our fault; this is the legacy of C that multiple languages double down on at this point. But it’s also not right. We say "the type has members A and B" then we pretend that A|B is also a member of the type. But that’s immediately subverting type safety: we have confused representation with naming.

So what’s the alternative? Well unfortunately, std::bitset doesn’t cut it, and in all probability can’t be made to cut it. But a better bitset could, and is totally implementable. Here’s what I’d like it to look like.

enum struct permissions {<br>READ, WRITE, EXEC,<br>MAX // here's the conventional opt-in<br>};

// bitset can take an enum, and preserve it (type safety)<br>using perms_t = bitset;

// how about a tag constructor?<br>auto p = perms_t{place_bits, permissions::READ, permissions::WRITE};<br>if (p[permissions::READ]) {<br>// ...

As far as I can see, this solves almost (I’m being conservative in saying almost) all the problems with the enumeration approach.

Type safety. I can’t assign a perms_t here from some other bitset or from an integral type. And this is true even for unscoped enums I find in 3rd party libraries.

Ergonomics of use. operator[] isn’t going to have the precedence problems of the bitwise operators. Enumeration values don’t have to be pre-shifted, avoiding a source of potential error.

Correct separation of concerns. The enumeration type names the bits. The bitset represents them. We aren’t pretending that READ|WRITE is a member of permissions.

It still ticks the other boxes too. Implementation is at least as easy as, if not easier than, the enum approach.

I think the one remaining problem is around the ergonomics of taking a 3rd party codebase whose enum(s) I don’t own, and treating the enums as bits. Fair criticism. I think we are always going to find that there are ways this can be annoying. With the enum bitflags approach I would use an intrusive opt-in mechanism something like injecting ADL-found functions into a 3rd party namespace. With a bitset approach, it’s not intrusive, but I still need to provide a "max" value (or otherwise specify the number of bits in some other API shape variation not shown here).

Both of these techniques mean that I might have to change code when upstream code changes. I think on balance the bitset approach is a bit easier, especially considering that we can probably use reflection to discover a...

enum bitset using like type permissions

Related Articles