Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Safely compare integer to strongly typed enum

Tags:

c++

c++11

How can one safely compare an integral value, of unknown type, to a strongly typed enum, when it's possible that the integral value may not be within the range of enumeration values?

The most obvious way to compare an integral value to an enum would be to cast the integral value, a, to the enum type, E, and compare to the enumeration value b, like this:

template <typename I, typename E>
bool compare(I a, E b) { return static_cast<E>(a) == b; }

However, if a is not in the range of enumeration values, this leads to unspecified behavior, per [expr.static.cast]/10:

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

This can be seen in the resulting failure (compare as above):

enum E : uint8_t { A = 0 };
compare(256, E::A); // returns true, 256 == E::A, but E::A = 0

One could instead cast the enum to the integral type, but this can lead to incorrect results if the integral type can not represent all the enum values:

enum E : int { A = 256 };
template <typename I, typename E>
bool compare(I a, E b) { return a == static_cast<I>(b); }
compare((uint8_t)0); // returns true, 0 == E::A, but E:A = 256
like image 694
TrentP Avatar asked Dec 03 '17 06:12

TrentP


1 Answers

The enum can be cast to its underlying integral type, which is guaranteed to be able to represent all of the enumeration's values.

template <typename I, typename E>
bool compare(I a, E b) { return a == static_cast<std::underlying_type_t<E>>(b); }

There's still a possible issue with the usual arithmetic conversions if the integral type and the enumeration type differ in signededness.

enum class : int32_t { A = -1 };
compare(4294967295u, E3::A); // returns true

Here, E::A = (int32_t)-1 is converted to unsigned int, which can not represent -1, converting it to (most likely) 4294967295.

This conversion, of an integer to another integral type which can not represent its value, can only happen if one type is unsigned and the other has a negative value (and thus must be of signed type). Since an unsigned value and a negative value cannot possibly be equal, we can tell the result of the comparison without needing to compare the exact values.

template <typename I, typename E>
bool compare(I a, E b) {
    using UTE = std::underlying_type_t<E>; 
    return !(std::is_unsigned_v<I> && static_cast<UTE>(b) < 0) &&
           !(std::is_unsigned_v<UTE> && a < 0) &&
           a == static_cast<UTE>(b);
}

This will correctly catch the cases were a negative value would be converted to an unsigned value that could then match the other operand. Since the compiler knows the types at compile time, it can optimize the sign check expression to nothing, a<0, or b<0 as appropriate for the types of a and b.

like image 114
TrentP Avatar answered Oct 04 '22 22:10

TrentP