8
\$\begingroup\$

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?

\$\endgroup\$
1

1 Answer 1

5
\$\begingroup\$

Simplify is_bitmask

Since you are using C++20 anyway, you can avoid making is_bitmask a struct, and make it a constexpr template variable instead. This simplifies the first part of the code a lot:

#define ENABLE_BITMASK(type) template<> \
inline constexpr bool is_bitmask<type> = true;

template <typename T>
inline constexpr bool is_bitmask = false;

template <typename T>
concept is_enum_bitmask = std::is_enum_v<T> && is_bitmask<T>;

Missing [[nodiscard]]s?

I see you use [[nodiscard]] for most of the operators. Of course it should not be used for |=, &= and ^=, but why not use it for ~, bool() and Type() as well?

Missing global operators

You have a just three global operators overloaded for bitmasks, and they all result in an EnumBitset<T> result. But I would also like to be able to write:

auto x = BitmaskableEnum::kFirst; 
auto y = ~x; // no match for operator~
auto z = !x; // no match for operator!

Missing noexcept

There are a few member functions that are missing noexcept. There's nothing that should be able to throw.

Unnecessary trailing return types

I don't see a reason for the trailing return types for the global operators. The leading auto return type will already do the right thing.

Putting it in a namespace

You can move everything into its own namespace, with some changes. First, preprocessor macros are expanded at the point they are used, so you have to make sure they declare is_bitmask in the right namespace:

namespace util {
#define ENABLE_BITMASK(type) template<> \
inline constexpr bool util::is_bitmask<type> = true;
...

Second, ADL is not the issue, but regular lookup is the problem. You have to tell the compiler that you want to search for functions inside the util namespace, either by doing:

using namespace util;

Or if you want to restrict it only to the operators, then you have to write:

using util::operator&;
using util::operator|;
using util::operator^;
...

Once the operators have been found and they return a util::EnumBitset<T>, the compiler already has the full name of the type and doesn't have to do any further lookups.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.