Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In pursuit of a better bitflag enum

Okay, so we're at C++ 17 and there still isn't a satisfactory answer to a really great bitflags interface in C++.

We have enum which bleed their member values into the enclosing scope, but do implicitly convert to their underlying type, so can be used as-if they were bit flags but refuse to reassign back into the enum without casting.

We have enum class which solves the name scope issue so that their values must be explicitly named MyEnum::MyFlag or even MyClass::MyEnum::MyFlag, but they do not implicitly convert to their underlying type, so cannot be used as bit-flags without endless casting back and forth.

And finally, we have the old bit-fields from C such as:

struct FileFlags {
   unsigned ReadOnly : 1;
   unsigned Hidden : 1;
   ...
};

Which has the drawback of having no good way to initialize itself as a whole - one has to resort to using memset or casting the address or similar to overwrite the entire value or initialize it all at once or otherwise manipulate multiple bits at once. It also suffers from not being able to name the value of a given flag, as opposed to its address - so there is no name representing 0x02, whereas there is such a name when using enums, hence it's easy with enums to name a combination of flags, such as FileFlags::ReadOnly | FileFlags::Hidden- there simply isn't a good way to say as much for bit-fields.

In addition we still have simple constexpr or #define to name bit values and then simply not use enums at all. This works, but completely dissociates the bit values from the underlying bitflag type. Perhaps this is ultimately not the worst approach, particularly if the bit flag values are constexpr within a struct to give them their own name-scope?

struct FileFlags {
    constexpr static uint16_t ReadOnly = 0x01u;
    constexpr static uint16_t Hidden = 0x02u;
    ...
}

So, as it currently stands, we have a lot of techniques, none of which add up to a really solid way to say

Here is a type which has the following valid bit-flags in it, it has its own name-scope, and these bits and type should be freely usable with standard bitwise operators such as | & ^ ~, and they should be comparable to integral values such as 0, and the result of any bitwise operators should remain the named type, and not devolve into an integral

All of that said, there are a number of attempts floating around to try to produce the above entity in C++ -

  1. The windows OS team developed a simple macro that generates C++ code to define the necessary missing operators on a given enum type DEFINE_ENUM_FLAG_OPERATORS(EnumType) which then defines operator | & ^ ~ and the associated assignment ops such as |= and etc.
  2. 'grisumbras' has a public GIT project for enabling bitflag semantics with scoped enums here, which uses enable_if meta programming to allow a given enum to convert to a bit-flag type which supports the missing operators and back again silently.
  3. Without knowing the above, I wrote a relatively simple bit_flags wrapper which defines all of the bitwise operators on itself, so that one can use a bit_flags<EnumType> flags and then flags has bitwise semantics. What this fails to do is allow the enumerated base to actually properly handle bitwise operators directly, so you cannot say EnumType::ReadOnly | EnumType::Hidden even when using a bit_flags<EnumType> because the underlying enum itself still doesn't support the necessary operators. I had to end up doing the same thing essentially as #1 and #2 above, and enabling operator | (EnumType, EnumType) for the various bitwise operators by requiring users to declare a specialization for a meta type for their enum such as template <> struct is_bitflag_enum<EnumType> : std::true_type {};

Ultimately, the problem with #1, #2, and #3 is that it is not possible (as far as I know) to define the missing operators on the enum itself (as in #1) or to define the necessary enabler type (e.g. template <> struct is_bitflag_enum<EnumType> : std::true_type {}; as in #2 and partially #3) at class scope. Those must happen outside of a class or struct, as C++ simply doesn't have a mechanism that I am aware of which would allow me to make such declarations within a class.

So now, I have the desire to have a set of flags that should be scoped to a given class, but I cannot use those flags within the class header (e.g. default initialization, inline functions, etc.) because I cannot enable any of the machinery that allows the enum to be treated as bitflags until after the closing brace for the class definition. Or, I can define all such flag-enums outside of the class that they belong to, so that I can then invoke the "make this enum into a bitwise type" before the user-class definition, to have full use of that functionality in the client class - but now the bit flags are in the outer scope instead of being associated to the class itself.

This isn't the end of the world - none of the above is. But all of it causes endless headaches when writing my code - and stops me from writing it in the most natural way - i.e. with a given flag-enum that belongs to a specific class within (scoped to) that client class, but with bitwise flag-semantics (my approach #3 almost allows this - so long as everything is wrapped by a bit_flags - to explicitly enable the needed bitwise compatibility).

All of this still leaves me with the annoying sense that this could be much better than it is!

There surely should be - and perhaps is but I haven't figured it out yet - approach to enums to enable bitwise operators on them while allowing them to be declared and used within an enclosing class scope...

Does anyone have a wip or an approach I haven't considered above, that would allow me "the best of all possible worlds" on this?

like image 875
Mordachai Avatar asked Apr 04 '18 14:04

Mordachai


People also ask

What is Flag enum in C#?

Flags enumerations are used for masking bit fields and doing bitwise comparisons. They are the correct design to use when multiple enumeration values can be specified at the same time.

How do bit flags work?

Bit flags are used for compact representation of small sets of values. Each value is assigned a bit index. All integer numbers with the bit at that index set to 1 are interpreted as sets that include the corresponding member. enum Color { Red = 1 << 0 , Green = 1 << 1 , Blue = 1 << 2 };

What is bitwise flag?

This is done by using the bitwise and shift operators to set, clear, and check individual bits in an integer, treating each bit as a separate boolean value. These individual bits are called bit flags. When talking about individual bits, we typically count from right to left, starting with leading “0” (zero).

What are bitflags and how do they work?

Another interesting property of bitflags is that you can combine multiple flags (or bits) together to express any combination of different states being set at the same time.

What are bit flags in C++?

They are basically a clever way to pack a lot of information inside a single variable, usually an integer or a plain unsigned byte. For example, if you want to enumerate all possible states of an enemy, using a bit flag unsigned byte value gives you 255 possibilities ! So usually much more than you will ever need.

Can an enum have more than 32 different labels?

Lastly, since enums are normally stored into , it’s unwise to have an enum with more then 32 different labels. . The only thing it does is allowing a nicer output of enums when they are printed. A bit mask is, essentially, an integer value in which several binary property (yes/no) are independently stored in its bit.

What can we do with enums?

If a public field is an enum, it will conveniently appear like a dropdown menu: The vast majority of developers use enums just as we’ve seen before. There’s much more we can do with them though. The first limitation is that standard enums can only hold a value at a time. What if we are using a melee weapon with a fire attack (a fire sword?!)?


1 Answers

You can have friend functions inside of an enclosing class that takes the enum as values. This can be used within a macro to define the necessary functions, all within a class scope.

For example, to avoid the is_bitflag_enum trait to specialize, specialize a struct which holds the enums and the operators. This is like #2, and still can't be done in a class.

#include <type_traits>

template<class Tag>
struct bitflag {
    enum class type;

#define DEFINE_BITFLAG_OPERATOR(OP) \
    friend constexpr type operator OP(type lhs, type rhs) noexcept { \
        typedef typename ::std::underlying_type<type>::type underlying; \
        return static_cast<type>(static_cast<underlying>(lhs) OP static_cast<underlying>(rhs)); \
    } \
    friend constexpr type& operator OP ## = (type& lhs, type rhs) noexcept { \
        return (lhs = lhs OP rhs); \
    }

    DEFINE_BITFLAG_OPERATOR(|)
    DEFINE_BITFLAG_OPERATOR(&)
    DEFINE_BITFLAG_OPERATOR(^)

#undef DEFINE_BITFLAG_OPERATOR

#define DEFINE_BITFLAG_OPERATOR(OP) \
    friend constexpr bool operator OP(type lhs, typename ::std::underlying_type<type>::type rhs) noexcept { \
        return static_cast<typename ::std::underlying_type<type>::type>(lhs) OP rhs; \
    } \
    friend constexpr bool operator OP(typename ::std::underlying_type<type>::type lhs, type rhs) noexcept { \
        return lhs OP static_cast<typename ::std::underlying_type<type>::type>(rhs); \
    }

    DEFINE_BITFLAG_OPERATOR(==)
    DEFINE_BITFLAG_OPERATOR(!=)
    DEFINE_BITFLAG_OPERATOR(<)
    DEFINE_BITFLAG_OPERATOR(>)
    DEFINE_BITFLAG_OPERATOR(>=)
    DEFINE_BITFLAG_OPERATOR(<=)

#undef DEFINE_BITFLAG_OPERATOR

    friend constexpr type operator~(type e) noexcept {
        return static_cast<type>(~static_cast<typename ::std::underlying_type<type>::type>(e));
    }

    friend constexpr bool operator!(type e) noexcept {
        return static_cast<bool>(static_cast<typename ::std::underlying_type<type>::type>(e));
    }
};

// The `struct file_flags_tag` (Which declares a new type) differentiates between different
// enum classes declared
template<> enum class bitflag<struct file_flags_tag>::type {
    none = 0,
    readable = 1 << 0,
    writable = 1 << 1,
    executable = 1 << 2,
    hidden = 1 << 3
};

using file_flags = bitflag<file_flags_tag>::type;

bool is_executable(file_flags f) {
    return (f & file_flags::executable) == 0;
}

You can also make a single macro to define every single friend function. This is like #1, but it is all within a class scope.

#include <type_traits>

#define MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(OP, ENUM_TYPE) \
    friend constexpr ENUM_TYPE operator OP(ENUM_TYPE lhs, ENUM_TYPE rhs) noexcept { \
        typedef typename ::std::underlying_type<ENUM_TYPE>::type underlying; \
        return static_cast<ENUM_TYPE>(static_cast<underlying>(lhs) OP static_cast<underlying>(rhs)); \
    } \
    friend constexpr ENUM_TYPE& operator OP ## = (ENUM_TYPE& lhs, ENUM_TYPE rhs) noexcept { \
        return (lhs = lhs OP rhs); \
    }

#define MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(OP, ENUM_TYPE) \
    friend constexpr bool operator OP(ENUM_TYPE lhs, typename ::std::underlying_type<ENUM_TYPE>::type rhs) noexcept { \
        return static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(lhs) OP rhs; \
    } \
    friend constexpr bool operator OP(typename ::std::underlying_type<ENUM_TYPE>::type lhs, ENUM_TYPE rhs) noexcept { \
        return lhs OP static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(rhs); \
    }


#define MAKE_BITFLAG_FRIEND_OPERATORS(ENUM_TYPE) \
    public: \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(|, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(&, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(^, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(==, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(!=, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(<, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(>, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(>=, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(<=, ENUM_TYPE) \
    friend constexpr ENUM_TYPE operator~(ENUM_TYPE e) noexcept { \
        return static_cast<ENUM_TYPE>(~static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(e)); \
    } \
    friend constexpr bool operator!(ENUM_TYPE e) noexcept { \
        return static_cast<bool>(static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(e)); \
    }

// ^ The above in a header somewhere

class my_class {
public:
    enum class my_flags {
        none = 0, flag_a = 1 << 0, flag_b = 1 << 2
    };

    MAKE_BITFLAG_FRIEND_OPERATORS(my_flags)

    bool has_flag_a(my_flags f) {
        return (f & my_flags::flag_a) == 0;
    }
};
like image 195
Artyer Avatar answered Sep 29 '22 07:09

Artyer