c++ – Enabling bitset-like behvior for enum classes (C++20)

I want to enable bitmask-like behavior (ie. overloaded operator|, operator& and operator^) for some enum classes. This is what I came up with:

#pragma once
#include <type_traits>

/// brief Marks an enum class as enabled for bitmask behavior.
/// param type The name of the enum for which to enable the bitmask.
/// details This macro works by specializing see{is_bitmask}.
#define ENABLE_BITMASK(type) template<> 
struct is_bitmask<type> { 
    constexpr static bool value = true; 
private: 
    is_bitmask() = delete; 
};

/// This is a marker struct to enable bitmask behavior for enum classes. Should be
/// automatically specialized by using see ENABLE_BITMASK. 
/// tparam T The enum class for which to enable this behavior.
template <typename T>
struct is_bitmask final {
    constexpr static bool value = false;
private:
    is_bitmask() = delete;
};

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

/// Enables bitmask behavior for enum classes by overloading operator|, operator&, operator^ and operator~.
/// tparam Bits The enum to turn into a bitmask. This template parameter is constrained by see is_enum_bitmask.
template <is_enum_bitmask 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;
    }

    constexpr EnumBitset(const EnumBitset& other) = default;
    constexpr EnumBitset(EnumBitset&& other) noexcept = default;
    constexpr EnumBitset& operator=(const EnumBitset& other) = default;
    constexpr EnumBitset& operator=(EnumBitset&& other) noexcept = default;
    constexpr ~EnumBitset() noexcept = default;
    
    ((nodiscard)) constexpr inline EnumBitset<Bits> operator|(const EnumBitset<Bits>& b) const noexcept {
        return EnumBitset{this->bits_ | b.bits_};
    }

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

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

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

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

    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_;
    }

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

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

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

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

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

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

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

Here’s how it’s supposed to work:

  • One can enable bitmask behavior by using ENABLE_BITMASK(MyEnum). I’m really unsure if this is a good way of doing this or if there are better ways to tag an enum as “bitmask enabled”. I don’t want to enable it for all enum classes since this would make enum classes pointless.
  • All enum class constants can be used with each other, but we can’t mix two enums.
  • All operations should be constexpr so they can be optimized by the compiler.
  • (Known Limitation): std::is_enum_v<T> works for both enum classes and enums, I’ll fix this with C++23’s std::is_scoped_enum<T> as soon as it’s available.

Here’s some example code:

enum class BitmaskableEnum {
  kFirst = 1,
  kSecond = 2,
  kThird = 4,

  kFirstAndSecond = 3
};
ENABLE_BITMASK(BitmaskableEnum);

enum class SecondBitmaskableEnum {
  kFirst = 1
};
ENABLE_BITMASK(SecondBitmaskableEnum);

enum class RegularEnum {
  kFirstBit = 1,
  kSecondBit = 2,
  kBoth = 3
};

void tests() {
  // Should compile
  auto a = BitmaskableEnum::kFirst 
            | BitmaskableEnum::kSecond 
            ^ BitmaskableEnum::kThird;

  a ^= BitmaskableEnum::kThird;
  // Should be true
  assert(a == BitmaskableEnum::kFirstAndSecond);

  // Those should not compile:
  const auto b = RegularEnum::kFirstBit | RegularEnum::kSecondBit;
  const auto c = BitmaskableEnum::kFirst | SecondBitmaskableEnum::kFirst;
}

I also have a minor question: I’ll be using this in a larger project of mine and would like to put this in the util namespace. Would this work with ADL? If not: Which parts need to be kept in the global namespace?