3

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?

Elliott
  • 2,603
  • 2
  • 18
  • 35
GLJeff
  • 139
  • 10

2 Answers2

11

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_casting 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 (i.e. : 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;
bolov
  • 72,283
  • 15
  • 145
  • 224
  • std::to_underlying requires c++23, so I still have to use the cumbersome static_cast::type> but I love the use of concepts and have adopted it. – GLJeff Jan 02 '23 at 05:41
  • 1
    Also an interesting note, I asked a very popular coding guru this question and they responded: "Yes, it is undefined behavior to use values in an enum class that don't correspond to an existing enumerator. An enumerator is a named constant whose value is specified in the enum definition." Their name is ChatGpt. *shakes head* – GLJeff Jan 02 '23 at 06:16
  • 1
    @GLJeff I personally am not on the ChatGPT hate train I see around here. Although it's flawed I see it as an awesome technology, a proof of concept. If you blindly trust it and don't know how to check its output then yes it's dangerous. But if you used it smart it can actually be a useful tool. For instance I fed it my `operator|` and ask it to write all the operators. And it did. Correctly. Binary and unary operators. And compound assignment operators. I told it that `EnumFlag` is a concept for an enum and it knew to write just the binary operators relevant for enums. I find that impressive. – bolov Jan 02 '23 at 15:50
  • 1
    on the compound assignments it knew that the non assignment equivalent operators were defined and used them directly on the enum, didn't use the `to_underlying` and `static_cast`. I tell you, I am constantly amazed by it. And the fact that you can talk to it and make refinements. – bolov Jan 02 '23 at 15:53
  • 1
    And yes, there are so many situations where it fails and it is confident in its failing. But I cannot not see the purely amazing parts of it. – bolov Jan 02 '23 at 15:56
  • Point taken on ChatGPT. One other question and possible point of confusion for people: The standard you quoted specifically states "an enumeration whose underlying type is fixed" ... If the code does not include " : std::uint32_t" in the declaration, does it then become undefined behavior to use enum values out of range? Perhaps not 3 in this case, but if all the flags were combined to make a value greater than 4. – GLJeff Jan 02 '23 at 16:57
  • @GLJeff you still have all the values of the underlying type (I am 99% sure, I'll check when I get some time). – bolov Jan 02 '23 at 19:12
1

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".

supercat
  • 77,689
  • 9
  • 166
  • 211
  • This answer is in response to my comment requesting clarification on whether it would be undefined behavior to use enums outside of their explicitly requested range when the underlying type is not specified, correct? And the answer resolves to, yes, by definition it *technically* has to be. (Also in which case my question to ChatGPT was not specific enough, since I didn't specify whether or not the underlying type of the enum class was explicitly defined.) – GLJeff Jan 02 '23 at 18:52
  • @GLJeff: The Standards seek, for whatever reason, to avoid saying anything about actions whose behavioral characteristics can't be defined in *all* situations, even if in 99.9999% of practical situations they would be processed identically by all non-contrived implementations. – supercat Jan 02 '23 at 18:56