Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ enum class: Cast to non existing entry

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?

like image 308
bam Avatar asked Mar 28 '19 07:03

bam


2 Answers

This is fully safe, because:

  • your enum class is a scoped enumeration;
  • your enumeration has a fixed underlying type : char ;
  • so the values of your enumeration are the values of type char ;
  • so the cast of a char value to the enum is completely valid.

Here the C++17 standard quotes that correspond to the above statements:

[dcl.enum]/2: (...) The enum-keys enum class and enum 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;
}
like image 176
Christophe Avatar answered Oct 04 '22 20:10

Christophe


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.

like image 24
Sami Kuhmonen Avatar answered Oct 04 '22 18:10

Sami Kuhmonen