In 2020 I wrote an article for the German magazine iX called Scoped enums in C++. In that article, I shared an approach of using class enums as bitfields without the hassle of having to define the operators for each enum. The approach was inspired by Anthony William's post Using Enum Classes as Bitfields.
Today's post aims to bring you up to speed with the implementation in C++17 and then see how it transforms when you apply C++20 concepts to the code.
One operator for all binary operations of a kind
The idea is that the bit-operators are often used with enums to create bitmasks. Filesystem permissions are one example. Essentially you want to be able to write type-safe code like this:
Permission is a class enum, making the code type-safe. Now, all of you who once have dealt with class enums know that they come without support for operators. Which also is their strength. You can define the desired operator or operators for each enum. The issue here is that most of the code is the same. Cast the enum to the underlying type, apply the binary operation, and cast the result back to the enum type. Nothing terribly hard, but it is so annoying to repeatedly type it.
Anthony solved this by providing an operator, a function template that only gets enabled if you opt-in for a desired enum. Here is the implementation, including the definition of
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
Neat, isn't it?
The trick part is in the template-head in A. The
is_same together with
decltype and, of course,
std::declval checks that a function
enable_bitmask_operator_or exists for the given enum, which I provide in B. Well,
Let's use the code for
operator| and see how C++20 can simplify your code.
C++20's concepts applied
The great thing about C++20s concepts is that we can eliminate the often hard-to-digest
enable_if. Further, checking for functions' existence requires less code due to the
requires-expression of concepts.
Here is the same operator using C++20s concepts instead of the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
I can't tell you how much I like this code. No
conjunction, and no
declval. So beautiful.
The requires-expression tries to call
enable_bitmask_operator_or in A, together with the
is_enum_v, that's all that's required in C++20.
There is one other bonus in C++20. Since you have not only
constexpr but also
consteval functions available, applying them in B to
enable_bitmask_operator_or signals a bit better that this function is for compile-time purposes only.
C++23: The small pearl
One more thing. You have C++23 available now. There is one change you can now make to simplify the code even more. C++23 offers you
std::to_underlying for converting a class enum value to a value of its underlying type. The function is located in
<utility>. Applying this to the example leads to the following code:
1 2 3 4 5 6 7 8 9
Not only does
std::to_underlying remove redundant and boring code you had to write before C++23 but in my opinion, the utility function makes the code more readable as well.