简体   繁体   中英

Is using enum class for flags undefined behavior?

I've been using overloaded operators as demonstrated in the second answer from here: How to use C++11 enum class for flags ... example:

#define ENUMFLAGOPS(EnumName)\
[[nodiscard]] __forceinline EnumName operator|(EnumName lhs, EnumName rhs)\
{\
    return static_cast<EnumName>(\
        static_cast<std::underlying_type<EnumName>::type>(lhs) |\
        static_cast<std::underlying_type<EnumName>::type>(rhs)\
        );\
}...(other operator overloads)

enum class MyFlags : UINT //duplicated in JS
{
    None = 0,
    FlagA = 1,
    FlagB = 2,
    FlagC = 4,
};
ENUMFLAGOPS(MyFlags)

...

MyFlags Flags = MyFlags::FlagA | MyFlags::FlagB;

And I've grown concerned that this may be producing undefined behavior. I've seen it mentioned that merely having an enum class variable that is not equal to one of the defined enum values is undefined behavior. The underlying UINT value of Flags in this case is 3. Is this undefined behavior? And if so, what would be the right way to do this in c++20?

It's a misconception that an enum type has only the values it declares. Enums have all the values of the underlying type. It's just that in an enum some of these values have names. It's perfectly fine to obtain a value that has no name by static_cast ing or in the case of classical enums by operations ( | ) or simple assignment.

Your code is perfectly fine (outside of maybe raising some eyebrows for the macro use).

9.7.1 Enumeration declarations [dcl.enum]

  1. For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type.

For enumerations whose underlying type is not fixed (ie : std::uint32_t is missing) the standard says basically the same thing, but in a more convoluted way: the enum has the same values as the underlying type, but there are more rules about what the underlying type is.


This is outside the scope of your question but you can define your operators without any macros and I highly recommend it:

template <class E>
concept EnumFlag = std::is_enum_v<E> && requires() { {E::FlagTag}; };

template <EnumFlag E>
[[nodiscard]] constexpr E operator|(E lhs, E rhs)
{
    return static_cast<E>(std::to_underlying(lhs) | std::to_underlying(rhs));
}

enum class MyFlags : std::uint32_t
{
    None = 0x00,
    FlagA = 0x01,
    FlagB = 0x02,
    FlagC = 0x04,

    FlagTag = 0x00,
};

Yes, you can have multiple "names" (enumerators) with the same value. Because we don't don't use the FlagTag value it doesn't matter what value it has.

To mark enums for which you want the operators defined you can use a tag like in the above example or you can use a type trait:

template <class E>
struct is_enum_flag : std::false_type {};

template <>
struct is_enum_flag<MyFlags> : std::true_type {};

template <class E>
concept EnumFlag = is_enum_flag<E>::value;

Neither the C nor C++ Standard make any distinction between actions which would be Undefined Behavior if examined in isolation without knowledge of things like types' bit patterns and associated trap representations (or lack thereof), versus those whose "Undefinedness" trumps any knowledge one might have about such things. This philosophy is perhaps best illustrated by the way the C99 Standard changed the treatment of x<<1 when x is negative; I the C++ Standard may have some better examples, but I'm not as familiar with it.

If a platform had an 8-bit store instruction that was faster than the normal ones except that attempting to store a bit pattern of 1100 0000 would cause the CPU to overheat and melt, I don't think anything in the C++ Standard would forbid a C++ implementation from offering an int_least7_t extended type that uses that store instruction, and using such a type to represent an enum whose type was unspecified, and whose values included -63 and -62, but not -64. If one couldn't be certain that code would not be run on such a platform, one couldn't know whether attempting to execute myEnum1 = (myEnumType)((int)myEnum2 & (int)myEnum3); when myEnum2 and myEnum3 hold -63 and -62, respectively, might set the CPU on fire. Thus the latter construct would be--as far as the Standard is concerned--Undefined Behavior.

Both the C and C++ Standards are caught between a rock and a hard place between some people who think there should be no need for the Standard to add new text to say that constructs which had been processed consistently for decades should continue to be, and others who view the lack of any such mandate as an invitation to throw longstanding practices out the window. The only way one can know whether any particular construct should be expected to work is to know whether the people responsible for target implementations respect precedent or view it as an impediment to "optimization".

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM