Why Does Casting Int to Invalid Enum Value Not Throw Exception

Why does casting int to invalid enum value NOT throw exception?

Taken from Confusion with parsing an Enum

This was a decision on the part of the people who created .NET. An enum is backed by another value type (int, short, byte, etc), and so it can actually have any value that is valid for those value types.

I personally am not a fan of the way this works, so I made a series of utility methods:

/// <summary>
/// Utility methods for enum values. This static type will fail to initialize
/// (throwing a <see cref="TypeInitializationException"/>) if
/// you try to provide a value that is not an enum.
/// </summary>
/// <typeparam name="T">An enum type. </typeparam>
public static class EnumUtil<T>
where T : struct, IConvertible // Try to get as much of a static check as we can.
{
// The .NET framework doesn't provide a compile-checked
// way to ensure that a type is an enum, so we have to check when the type
// is statically invoked.
static EnumUtil()
{
// Throw Exception on static initialization if the given type isn't an enum.
Require.That(typeof (T).IsEnum, () => typeof(T).FullName + " is not an enum type.");
}

/// <summary>
/// In the .NET Framework, objects can be cast to enum values which are not
/// defined for their type. This method provides a simple fail-fast check
/// that the enum value is defined, and creates a cast at the same time.
/// Cast the given value as the given enum type.
/// Throw an exception if the value is not defined for the given enum type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="enumValue"></param>
/// <exception cref="InvalidCastException">
/// If the given value is not a defined value of the enum type.
/// </exception>
/// <returns></returns>
public static T DefinedCast(object enumValue)

{
if (!System.Enum.IsDefined(typeof(T), enumValue))
throw new InvalidCastException(enumValue + " is not a defined value for enum type " +
typeof (T).FullName);
return (T) enumValue;
}

/// <summary>
///
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
public static T Parse(string enumValue)
{
var parsedValue = (T)System.Enum.Parse(typeof (T), enumValue);
//Require that the parsed value is defined
Require.That(parsedValue.IsDefined(),
() => new ArgumentException(string.Format("{0} is not a defined value for enum type {1}",
enumValue, typeof(T).FullName)));
return parsedValue;
}

public static bool IsDefined(T enumValue)
{
return System.Enum.IsDefined(typeof (T), enumValue);
}

}


public static class EnumExtensions
{
public static bool IsDefined<T>(this T enumValue)
where T : struct, IConvertible
{
return EnumUtil<T>.IsDefined(enumValue);
}
}

This way, I can say:

if(!sEnum.IsDefined()) throw new Exception(...);

... or:

EnumUtil<Stooge>.Parse(s); // throws an exception if s is not a defined value.

Edit

Beyond the explanation given above, you have to realize that the .NET version of Enum follows a more C-inspired pattern than a Java-inspired one. This makes it possible to have "Bit Flag" enums which can use binary patterns to determine whether a particular "flag" is active in an enum value. If you had to define every possible combination of flags (i.e. MondayAndTuesday, MondayAndWednesdayAndThursday), these would be extremely tedious. So having the capacity to use undefined enum values can be really handy. It just requires a little extra work when you want a fail-fast behavior on enum types that don't leverage these sorts of tricks.

Casting to invalid enum value, Enum.ToObject, does not throw exception, sets Enum to int

Just use Enum.IsDefined. Basically enums are just ints and you can assign any int to an enum even if it isn't defined.

if(!Enum.IsDefined(typeof(A), a))
{
throw new InvalidCastException("Not a valid value for A: " + a);
}

Assign invalidValue to Enum variable (why is this not throwing an Exception?)

According to ISO IEC 23270 (2006):

In particular, any value of the underlying type of an enum can be cast
to the enum type, and is a distinct valid value of that enum type.

Enumeration does not "inherit" from underlying type (it's only a syntax). According to the same ISO, enumeration implicitly inherits from System.Enum and System.ValueType.

The type System.Enum is the abstract base class of all enum types (this is distinct and different from the underlying type of the enum
type)

Note that System.Enum is not itself an enum-type. Rather, it is a
class-type from which all enum-types are derived. The type System.Enum
inherits from the type System.ValueType (§11.1.1), which, in turn,
inherits from type object

If you want to check if enum's value is valid in "common" sense, you can use Enum.IsDefined method.

Why don't I get InvalidCastException when casting enum to integer fails?

Because it's not an invalid cast.

The value you are casting is out of range for the enum (in this case) but it's not invalid.

As the approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong a cast from integer to enum is perfectly legal.

Source - MSDN

Casting an out-of-range number to an enum in C# does not produce an exception

In C#, unlike Java, enums are not checked. You can have any value of the underlying type. This is why it's pretty important to check your input.

if(!Enum.IsDefined(typeof(MyEnum), value))
throw new ArgumentOutOfRangeException();

int to enum invalid conversion

Enumerations are just a fancy way to work with values of the underlying datatype - Int32 by default. You get named values, you lose mathematical operations. There is no rule that only values named in the declaration are valid values and therefore you get no exception or compiler error.

This becomes really obvious when you use enumerations as flags.

[Flags]
public enum Something
{
Foo = 1,
Bar = 2,
Baz = 4
}

Now something like this

var x = Something.Foo | Something.Bar; // The value is 1 | 4 = 5

is absolutely fine and again you can have values you never named. It all boils down to your wrong assumption that the set of allowed values is in someway restricted and as pointed out by other answers referring to the specification this is just not the case.

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.



Related Topics



Leave a reply



Submit