I have this situation on one Project where we have some socket-communication that mainly exchanges characters for flow-control.
We cast those characters to an enum class : char
in a switch.
I was wondering, what might happen, if the other end sends an character that is not in our enum class.
I have this mwe:
enum class Foo : char {
UNKNOWN,
ENUM1 = 'A',
ENUM2 = 'B',
ENUM3 = 'C'
};
char bar1() {
return 'B';
}
char bar2() {
return 'D';
}
int main() {
switch((Foo)bar1()) {
case Foo::UNKNOWN:std::cout << "UNKNWON" << std::endl;break;
case Foo::ENUM1:std::cout << "ENUM1" << std::endl;break;
case Foo::ENUM2:std::cout << "ENUM2" << std::endl;break;
case Foo::ENUM3:std::cout << "ENUM3" << std::endl;break;
default:std::cout << "DEFAULT" << std::endl;break;
}
switch((Foo)bar2()) {
case Foo::UNKNOWN:std::cout << "UNKNWON" << std::endl;break;
case Foo::ENUM1:std::cout << "ENUM1" << std::endl;break;
case Foo::ENUM2:std::cout << "ENUM2" << std::endl;break;
case Foo::ENUM3:std::cout << "ENUM3" << std::endl;break;
default:std::cout << "DEFAULT" << std::endl;break;
}
return 0;
}
In this example I have an enum class : char
with an unspecified entry and three char-assigned entries. When I run it, the output I receive is
ENUM2
DEFAULT
This seems to work flawlessly since the undefined example just jumps to the default case. However, is this "save to do"? Are there some pitfalls or other complications that I might not see right now?
This is fully safe, because:
enum class
is a scoped enumeration; : char
;char
; Here the C++17 standard quotes that correspond to the above statements:
[dcl.enum]/2: (...) The enum-keys
enum class
andenum struct
are semantically equivalent; an enumeration type declared with one of these is a scoped enumeration, and its enumerators are scoped enumerators.[dcl.enum]/5: (...) Each enumeration also has an underlying type. The underlying type can be explicitly specified using an enum-base. (...) In both of these cases, the underlying type is said to be fixed. (...)
[dcl.enum]/8: For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type. (...)
[expr.static.cast]/10 A value of integral or enumeration type can be explicitly converted to a complete enumeration type. If the enumeration type has a fixed underlying type, the value is first converted to that type by integral conversion, if necessary, and then to the enumeration type. [expr.cast]/4 The conversions performed by a const_cast, a static_cast, a static_cast followed by a const_cast, a reinterpret_cast, a reinterpret_cast followed by a const_cast, can be performed using the cast notation of explicit type conversion. (...) If a conversion can be interpreted in more than one of the ways listed above, the interpretation that appears first in the list is used (...)
The conclusions would be different if the underlying type would not be fixed. In this case, the remaining part of [dcl.enum]/8 would apply: it says more or less that if you're not within the smallest and the largest enumerators of the enumeration, you're not sure that the value can be represented.
See also the question Is it allowed for an enum to have an unlisted value?, which is more general (C++ & C) but doesn't use a scoped enum nor a specified underlying type.
And here a code snippet to use the enum values for which there is no enumerator defined:
switch((Foo)bar2()) {
case Foo::UNKNOWN: std::cout << "UNKNWON" << std::endl;break;
case Foo::ENUM1: std::cout << "ENUM1" << std::endl;break;
case Foo::ENUM2: std::cout << "ENUM2" << std::endl;break;
case Foo::ENUM3: std::cout << "ENUM3" << std::endl;break;
case static_cast<Foo>('D'): std::cout << "ENUM-SPECIAL-D" << std::endl;break;
default: std::cout << "DEFAULT" << std::endl;break;
}
It is not entirely safe. What I found is that C++ Standard, [expr.static.cast], paragraph 10, states the following:
A value of integral or enumeration type can be explicitly converted to an enumeration type. The value is unchanged if the original value is within the range of the enumeration values (7.2). Otherwise, the resulting value is unspecified (and might not be in that range). A value of floating-point type can also be explicitly converted to an enumeration type. The resulting value is the same as converting the original value to the underlying type of the enumeration (4.9), and subsequently to the enumeration type.
The 7.2 section explains how the limits are determined:
For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type. Otherwise, for an enumeration where emin is the smallest enumerator and emax is the largest, the values of the enumeration are the values in the range bmin to bmax, defined as follows: Let K be 1 for a two’s complement representation and 0 for a one’s complement or sign-magnitude representation. bmax is the smallest value greater than or equal to max(|emin| − K, |emax|) and equal to 2M − 1, where M is a non-negative integer. bmin is zero if emin is non-negative and −(bmax + K) otherwise. The size of the smallest bit-field large enough to hold all the values of the enumeration type is max(M, 1) if bmin is zero and M + 1 otherwise. It is possible to define an enumeration that has values not defined by any of its enumerators. If the enumerator-list is empty, the values of the enumeration are as if the enumeration had a single enumerator with value 0.
So it might work casting an undefined value into an enum if it's within the range, but if it's not then it is undefined. Theoretically one could define an enum with a big value and make sure the casting then works, but it might be better to cast backwards, from an enum to the integral type and compare that.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With