Cro: Maintain it With Zig
Cro: Maintain it With Zig
Posted Sep 12, 2021 11:34 UTC (Sun) by excors (subscriber, #95769)In reply to: Cro: Maintain it With Zig by HelloWorld
Parent article: Cro: Maintain it With Zig
I think that's incorrect, because it's legal to cast an integer to an "enum class" type even if it's not one of the declared enumerators. Then it wouldn't match any of your 'exhaustive' cases and you need to handle it with a default case (or intentionally rely on the default default behaviour of falling off the bottom of the switch).
(This surprised me when I discovered it recently.)
Specifically, according to C++17 every enum has an 'underlying type'. For "enum E : T {}" and "enum class E : T {}", it is 'fixed' as T. For "enum class E {}", it is fixed as int. The 'values of the enumeration' are the values of the underlying type, e.g. for "enum class E {}" it's all values of type int.
For "enum E {}", the underlying type is not fixed and is implementation-defined. The values of the enumeration are (basically) from 0 up to the smallest 2^N-1 that will fit all the defined enumerators, which may be a smaller range than the underlying type.
With a fixed underlying type, casting an integral value to the enumeration type will convert it to the underlying type first, by the usual integer rules. That means it will always be one of the values of the enumeration, so the cast is always allowed.
With a non-fixed underlying type, casting is only allowed if the integral value is within the range of the enumeration values. E.g. if you have "enum E { one=1, six=6 };" then the range is 0 to 2^N-1 with N=3, so (E)2 and (E)7 are permitted but (E)8 is undefined behaviour. Clang's UndefinedBehaviorSanitizer helpfully detects that: "runtime error: load of value 8, which is not a valid value for type 'E'".
(That restriction is specifically for casting - the standard says "It does not preclude an expression of enumeration type from having a value that falls outside this range". I guess something like "std::underlying_type_t<E> n = 8; E e; memcpy(&e, &n, sizeof(e));" might be a legal way to generate such a value, but I'm not familiar enough with the rules to be certain.)
So I think about the only situation where you can exhaustively switch on an enum without a default case, is when it's "enum E : uint8_t" / "enum class E : uint8_t" and you define enumerators for every value from 0 to 255. In all other cases, for both "enum" and "enum class", it's perfectly legal to have values of the enumeration type that are not one of the enumerators. You need to either do some global analysis of your program (which is outside the scope of C++) to prove you never generate such values, or write code that's locally safe by handling the default case in every switch.
This does make the compiler's "warning: enumeration value '...' not handled in switch" warnings quite silly, because if you forget to handle one enumerator but have a default case you won't get that warning, and if you remove the default case (to enable the warning) and handle every enumerator (to fix the bug revealed by that warning) then the warning goes away even though you've just added billions of unhandled enumeration values. In the latter case, at least GCC will sometimes still warn you that "control reaches end of non-void function" despite you handling every declared enumator - Clang suppresses that warning and silently generates code that will trigger undefined behaviour at runtime when given a valid enumeration value that doesn't match any of the supposedly-exhaustive cases.
