What Happens If You Static_Cast Invalid Value to Enum Class

What happens if you static_cast invalid value to enum class?

What is color set to according to the standard?

Answering with a quote from the C++11 and C++14 Standards:

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

Let's look up the range of the enumeration values: [dcl.enum]/7

For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type.

Before CWG 1766 (C++11, C++14)
Therefore, for data[0] == 100, the resulting value is specified(*), and no Undefined Behaviour (UB) is involved. More generally, as you cast from the underlying type to the enumeration type, no value in data[0] can lead to UB for the static_cast.

After CWG 1766 (C++17)
See CWG defect 1766.
The [expr.static.cast]p10 paragraph has been strengthened, so you now can invoke UB if you cast a value that is outside the representable range of an enum to the enum type. This still doesn't apply to the scenario in the question, since data[0] is of the underlying type of the enumeration (see above).

Please note that CWG 1766 is considered a defect in the Standard, hence it is accepted for compiler implementers to apply to to their C++11 and C++14 compilation modes.

(*) char is required to be at least 8 bit wide, but isn't required to be unsigned. The maximum value storable is required to be at least 127 per Annex E of the C99 Standard.


Compare to [expr]/4

If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.

Before CWG 1766, the conversion integral type -> enumeration type can produce an unspecified value. The question is: Can an unspecified value be outside the representable values for its type? I believe the answer is no -- if the answer was yes, there wouldn't be any difference in the guarantees you get for operations on signed types between "this operation produces an unspecified value" and "this operation has undefined behaviour".

Hence, prior to CWG 1766, even static_cast<Color>(10000) would not invoke UB; but after CWG 1766, it does invoke UB.


Now, the switch statement:

[stmt.switch]/2

The condition shall be of integral type, enumeration type, or class type. [...] Integral promotions are performed.

[conv.prom]/4

A prvalue of an unscoped enumeration type whose underlying type is fixed (7.2) can be converted to a prvalue of its underlying type. Moreover, if integral promotion can be applied to its underlying type, a prvalue of an unscoped enumeration type whose underlying type is fixed can also be converted to a prvalue of the promoted underlying type.

Note: The underlying type of a scoped enum w/o enum-base is int. For unscoped enums the underlying type is implementation-defined, but shall not be larger than int if int can contain the values of all enumerators.

For an unscoped enumeration, this leads us to /1

A prvalue of an integer type other than bool, char16_t, char32_t, or wchar_t whose integer conversion rank (4.13) is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int.

In the case of an unscoped enumeration, we would be dealing with ints here. For scoped enumerations (enum class and enum struct), no integral promotion applies. In any way, the integral promotion doesn't lead to UB either, as the stored value is in the range of the underlying type and in the range of int.

[stmt.switch]/5

When the switch statement is executed, its condition is evaluated and compared with each case constant. If one of the case constants is equal to the value of the condition, control is passed to the statement following the matched case label. If no case constant matches the condition, and if there is a default label, control passes to the statement labeled by the default label.

The default label should be hit.

Note: One could take another look at the comparison operator, but it is not explicitly used in the referred "comparison". In fact, there's no hint it would introduce UB for scoped or unscoped enums in our case.


As a bonus, does the standard make any guarantees as about this but with plain enum?

Whether or not the enum is scoped doesn't make any difference here. However, it does make a difference whether or not the underlying type is fixed. The complete [decl.enum]/7 is:

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.

Let's have a look at the following enumeration:

enum ColorUnfixed /* no fixed underlying type */
{
red = 0x1,
yellow = 0x2
}

Note that we cannot define this as a scoped enum, since all scoped enums have fixed underlying types.

Fortunately, ColorUnfixed's smallest enumerator is red = 0x1, so max(|emin| − K, |emax|) is equal to |emax| in any case, which is yellow = 0x2. The smallest value greater or equal to 2, which is equal to 2M - 1 for a positive integer M is 3 (22 - 1). (I think the intent is to allow the range to extent in 1-bit-steps.) It follows that bmax is 3 and bmin is 0.

Therefore, 100 would be outside the range of ColorUnfixed, and the static_cast would produce an unspecified value before CWG 1766 and undefined behaviour after CWG 1766.

static_cast to enum class from underlying type value and switch for compiler assistance

It is legal C++.

The drawback is the DRY violation, but avoiding it is difficult.

In c++23 we'll have reflection and be able to generate equivalent code (without having to rely on compiler warnings to make sure we didn't miss any). Reflection syntax is still a bit in flux, but every version I have read over was able to handle that problem.

static_cast on integer to enum conversion

As usual, the compiler is just trying to keep you from shooting yourself in the foot. That's why you cannot just pass an int to a function expecting an enum. The compiler will rightfully complain, because the int might not match any valid enum value.

By adding the cast you basically tell the compiler 'Shut up, I know what I am doing'. What you are communicating here is that you are sure that the value you pass in is 'within the range of the enumeration values'. And you better make sure that is the case, or you are on a one-way trip to undefined-behavior-land.

If this is so dangerous, then why doesn't the compiler add a runtime check for the integer value? The reason is, as so often with C++, performance. Maybe you just know from the surrounding program logic that the int value will always be valid and you absolutely cannot waste any time on stupid runtime checks. From a language-design point of view, this might not be the most reasonable default to chose, especially when your goal is writing robust code. But that's just how C++ works: A developer should never have to pay for functionality that they might not want to use.

C++ enum class: Cast to non existing entry

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;
}

static_cast from enum to long yields different output depending on enum members

The solution, as stated in the code is to temporary convert to a signed integer variable and then make a static_cast to long.

int val2 = l2;

long lval2 = static_cast<long>(val2);


Related Topics



Leave a reply



Submit