C++20 Concepts Applied – Safe Bitmasks Using Scoped Enums

C++20 Concepts Applied – Safe Bitmasks Using Scoped Enums

By Andreas Fertig

Overload, 32(179):7-8, February 2024


It can be hard to follow code using enable_if. Andreas Fertig gives a practical example where C++20’s concepts can be used instead.

In 2020 I wrote an article for the German magazine iX called ‘Scoped enums in C++’ [Fertig20]. 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’ [Williams15].

Today’s article 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:

  using Filesystem::Permission;
  Permission readAndWrite{
    Permission::Read | Permission::Write};

The enum 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. Listing 1 is the implementation, including the definition of Permission.

template<typename T>
constexpr std::
  enable_if_t<
    std::conjunction_v<std::is_enum<T>,
      // look for enable_bitmask_operator_or
      // to  enable this operator 
      std::is_same<bool,
        decltype(enable_bitmask_operator_or(
          std::declval<T>()))>>,
  T>
operator|(const T lhs, const T rhs) {
  using underlying = std::underlying_type_t<T>;
  return static_cast<T>(
    static_cast<underlying>(lhs) |
    static_cast<underlying>(rhs));
}
namespace Filesystem {
  enum class Permission : uint8_t {
    Read = 1,
    Write,
    Execute,
  };
  // Opt-in for operator| 
  constexpr bool 
    enable_bitmask_operator_or(Permission);
} // namespace Filesystem
Listing 1

Neat, isn’t it?

The trick part is in the template-head in . 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 . Well, enable_if.

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.

Listing 2 is the same operator using C++20s concepts instead of the enable_if.

template<typename T>
requires(std::is_enum_v<T>and requires(T e) {
  // look for enable_bitmask_operator_or to
  // enable this operator 
  enable_bitmask_operator_or(e);
}) constexpr auto
operator|(const T lhs, const T rhs) {
  using underlying = std::underlying_type_t<T>;
  return static_cast<T>(
    static_cast<underlying>(lhs) |
    static_cast<underlying>(rhs));
}
namespace Filesystem {
  enum class Permission : uint8_t {
    Read    = 0x01,
    Write   = 0x02,
    Execute = 0x04,
  };
  // Opt-in for operator| 
  consteval 
    void enable_bitmask_operator_or(Permission);
} // namespace Filesystem
Listing 2

I can’t tell you how much I like this code. No decltype, no is_same, no conjunction, and no declval. So beautiful.

The requires-expression tries to call enable_bitmask_operator_or in , 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 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 now1. 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 code in Listing 3.

template<typename T>
requires(std::is_enum_v<T>and requires(T e) {
  enable_bitmask_operator_or(e);
}) constexpr auto
operator|(const T lhs, const T rhs)
{
  return static_cast<T>(std::to_underlying(lhs) |
                        std::to_underlying(rhs));
}
Listing 3

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.

References

[Fertig20] Andreas Fertig, ‘Scoped Enums in C++11’ in iX, accessible from https://www.heise.de/select/ix/2020/7/2006907575811763393

[Williams15] Anthony Williams ‘Using Enum Classes as Bitfields’, posted 29 January 2015 at https://www.justsoftwaresolutions.co.uk/cplusplus/using-enum-classes-as-bitfields.html

Footnote

  1. Check which parts of C++23 are available on your chosen compiler at https://en.cppreference.com/w/cpp/compiler_support

Andreas Fertig is a trainer and lecturer on C++11 to C++20, who presents at international conferences. Involved in the C++ standardization committee, he has published articles (for example, in iX) and several textbooks, most recently Programming with C++20. His tool – C++ Insights (https://cppinsights.io) – enables people to look behind the scenes of C++, and better understand constructs.

This article was first published on Andreas Fertig’s blog on 2 January 2024: https://andreasfertig.blog/2024/01/cpp20-concepts-applied/






Your Privacy

By clicking "Accept All Cookies" you agree ACCU can store cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

By clicking "Share IP Address" you agree ACCU can forward your IP address to third-party sites to enhance the information presented on the site, and that these sites may store cookies on your device.