I think using enumerations as flags is fine. The whole argument that 'its bad because they're not supposed to be used that way' I think is kinda silly. Sure you don't want to have a situation where you accidentally create an undefined bit pattern, but whether that bit pattern is an 'enum' or just an uint32_t, you still have the same error. It'll be the same problem in the same piece of code. The nice thing about enum's is you can have nicer names and avoid stuff like VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_NV.
Here is what I use (formatting is a bit off but you get the idea):
// --------------------------------------------------------------------------------------------------------------------------
// enumeration expansion
// - ENUM_CLASS_OPERATORS defines standard bit operators for enum class types
// - ENUM_CLASS_AND_OR defines only 'and' and 'or'
// --------------------------------------------------------------------------------------------------------------------------
# define ENUM_CLASS_OPERATORS(T) \
inline constexpr T operator~(T a) noexcept { return static_cast<T>(~static_cast<uint64_t>(a)); } \
inline constexpr T operator&(T a, T b) noexcept { return static_cast<T>(static_cast<uint64_t>(a) & static_cast<uint64_t>(b)); } \
inline constexpr T operator|(T a, T b) noexcept { return static_cast<T>(static_cast<uint64_t>(a) | static_cast<uint64_t>(b)); } \
inline constexpr T operator^(T a, T b) noexcept { return static_cast<T>(static_cast<uint64_t>(a) ^ static_cast<uint64_t>(b)); } \
inline T& operator&=(T& a, T b) noexcept { return a = static_cast<T>(static_cast<uint64_t>(a) & static_cast<uint64_t>(b)); } \
inline T& operator|=(T& a, T b) noexcept { return a = static_cast<T>(static_cast<uint64_t>(a) | static_cast<uint64_t>(b)); } \
inline T& operator^=(T& a, T b) noexcept { return a = static_cast<T>(static_cast<uint64_t>(a) ^ static_cast<uint64_t>(b)); }
# define ENUM_CLASS_AND_OR(T) \
inline constexpr T operator&(T a, T b) noexcept { return static_cast<T>(static_cast<uint64_t>(a) & static_cast<uint64_t>(b)); } \
inline constexpr T operator|(T a, T b) noexcept { return static_cast<T>(static_cast<uint64_t>(a) | static_cast<uint64_t>(b)); } \
inline T& operator&=(T& a, T b) noexcept { return a = static_cast<T>(static_cast<uint64_t>(a) & static_cast<uint64_t>(b)); } \
inline T& operator|=(T& a, T b) noexcept { return a = static_cast<T>(static_cast<uint64_t>(a) | static_cast<uint64_t>(b)); }
The thing to consider is, even with bit operators, its actually quite hard to come up with a bit pattern that's undefined. Most of code with flags looks something like:
enum class EEnumOptions {
none,
option_1,
option_2,
option_3,
};
enum class EEnumFlags {
none = 0,
flag_a = 1,
flag_b = 2,
flag_c = 4,
};
ENUM_CLASS_AND_OR(EEnumFlags) // create 'and' and 'or' bit operators for EEnumName
// ....
void Func(EEnumOptions e, EEnumFlags f) {
// handle options
switch (e) {
case EEnumOptions::option_1:
case EEnumOptions::option_2:
case EEnumOptions::option_3:
}
// handle flags
if ((f & EEnumName::flag_a) == EEnumName::flag_a) {} // flag_a is set
if ((f & EEnumName::flag_b) == EEnumName::flag_b) {} // flag_b is set
if ((f & EEnumName::flag_c) != EEnumName::flag_c) {} // flag_c is not set
}
Even if you were to make a silly bit pattern, things won't 'blow up'. Any bit pattern is still well defined. Also if you only restrict yourself to 'and' and 'or' (ie. don't overload 'not' and 'xor'), then its near impossible to create undefined bit patterns (short of intentionally static_cast'ing them in). Its still safer then simply integer constants or #define's, and for the most part self documenting. I don't think anyone would have any difficulty using that function, or understanding what is expected, and passing an undefined bit pattern would have to be intentional.
Maybe its my own personal preference, but this seems clean, easy to understand, and hard to break; and isn't that what we want?