Using enum classes as bitmasks

by

Feedback wanted!

I’m always glad about feedback. If you spot an error in this post, let me know and I’ll fix it.

This is just a quick tip on making enum classes a little bit cooler in C++. Consider the following code:

enum class MyBitField: uint16_t {
    eFoo = 1 << 1,
    eBar = 1 << 2,
    eBaz = 1 << 3,
    //... and so on
}

//Fails to compile!
auto fooAndBar1 = MyBitField::eFoo | MyBitField::eBar; 

//Compiles, but contains *a lot* of noise.
auto fooAndBar2 =   static_cast<uint16_t>(MyBitField::eFoo) 
                  | static_cast<uint16_t>(MyBitField::eBar);

Obviously this is intended to be used in expressions like fooAndBar1. Alas, this won’t compile since enum classes don’t cast to their underlying type. Thus, we’d have to write something like fooAndBar2. Not very nice to read, is it? Ideally, we could enable bitmask-like behaviour for some of our enum classes. We can achieve this by overloading operator| and the like. Ideally, our solution should also be constexpr wherever possible.

So lets step through the solution step-by-step. First, we create a new templated class, called EnumBitset. Its purpose is to hold the underlying type of the enum as a data member. It looks something like this:

template <typename Bits>
struct EnumBitset {
private:
    using Type = std::underlying_type_t<Bits>;
    Type bits_ = 0;

    constexpr EnumBitset(Type b) noexcept {
        bits_ = b;
    }

public:
    constexpr EnumBitset(Bits bit) noexcept {
        bits_ = static_cast<Type>(bit);
    }

    constexpr EnumBitset() noexcept {
        bits_ = 0;
    }

    //Rule of 5 c'tors
    constexpr EnumBitset(const EnumBitset& other) noexcept = default;
    constexpr EnumBitset(EnumBitset&& other) noexcept = default;
    constexpr EnumBitset& operator=(const EnumBitset& other) noexcept = default;
    constexpr EnumBitset& operator=(EnumBitset&& other) noexcept = default;
    constexpr ~EnumBitset() noexcept = default;
};

This class has three constructors that are used in different scenarios:

  • EnumBitset(Type b) is used when we want to construct an EnumBitset from the underlying type. This happens in places like operator|.
  • EnumBitset(Bits bit) is used for creating a bitset where we want to create a bitset from an enum value.
  • EnumBitset() just creates an empty bitset.

Now we can overload our bit-manipulation operators. For the sake of brevity I only show operator|, but the same applies for operator& and operator^.

template <typename Bits>
struct EnumBitset {
// ...
public:
    //Similarly operator& and operator^
    [[nodiscard]] 
    constexpr inline EnumBitset<Bits> operator|(const EnumBitset<Bits>& b) const noexcept {
        return EnumBitset{this->bits_ | b.bits_};
    }

    //Similarly operator&= and operator^=
    constexpr inline void operator|=(const EnumBitset<Bits>& b) noexcept {
        this->bits_ |= b.bits_;
    }

    [[nodiscard]] 
    constexpr inline bool operator==(const EnumBitset<Bits>& b) const noexcept {
        return this->bits_ == b.bits_;
    }

    [[nodiscard]] 
    constexpr inline bool operator!=(const EnumBitset<Bits>& b) const noexcept {
        return this->bits_ != b.bits_;
    }

    [[nodiscard]] 
    constexpr inline EnumBitset<Bits> operator~() const noexcept {
        return EnumBitset{~this->bits_};
    }

    [[nodiscard]] 
    constexpr inline operator bool() const noexcept {
        return bits_ != 0;
    }

    [[nodiscard]] 
    constexpr inline explicit operator Type() const noexcept {
        return bits_;
    }

    [[nodiscard]] 
    constexpr inline Type getBits() const { return bits_; }
};

This should be self explanatory. We implement all of the bit operators and the (in-)equality operators. Lastly, there are some cast operators. operator bool() serves as a any() function. We also can convert the bitset to its underlying type. There is no conversion back to the enum since it isn’t guaranteed that each value of the underlying type neatly maps to an enum value.

Right now however our template argument can be anything. We’re using C++20 so we can constrain it using template parameters. We can define a concept to enable bitset behavior selectively for some enums.

template <typename T>
concept is_enum_bitmask = std::is_enum_v<T> && (is_bitmask<T>::value == std::true_type::value);

template <is_enum_bitmask Bits>
struct EnumBitset { /* ... */ };

The last step is to have operators to convert an expression like Enum::kVal1 | Enum::kVal2 to a bitset automatically:

template <typename T, typename U>
    requires (is_enum_bitmask<T> && std::is_constructible_v<EnumBitset<T>, U>)
[[nodiscard]] constexpr auto operator|(T left, U right) {
    return EnumBitset<T>(left) | right;
}

template <typename T, typename U>
    requires (is_enum_bitmask<T> && std::is_constructible_v<EnumBitset<T>, U>)
[[nodiscard]] constexpr auto operator&(T left, U right) {
    return EnumBitset<T>(left) & right;
}

template <typename T, typename U>
    requires (is_enum_bitmask<T> && std::is_constructible_v<EnumBitset<T>, U>)
[[nodiscard]] constexpr auto operator^(T left, U right) {
    return EnumBitset<T>(left) ^ right;
}

template <typename T>
    requires (is_enum_bitmask<T>)
[[nodiscard]] constexpr auto operator!(T left) {
    return !EnumBitset<T>(left);
}

template <typename T>
    requires (is_enum_bitmask<T>)
[[nodiscard]] constexpr auto operator~(T left) {
    return EnumBitset<T>(left).operator~();
}

Essentially it uses EnumBitset<T>::EnumBitset(Bits bit) to create an EnumBitset from the left operand and calls the appropriate operator on the class.