Undefined behavior of constexpr static cast from int to scoped enum with non-fixed underlying type compiles in C++17

Viewed 213

I wonder if the following should or should not compile in C++17

enum class E
{
    A, B
};

constexpr E x = static_cast<E>(2);

This compiles with both GCC 9.3.0 and Clang 10.0.0 on Ubuntu 20.04.

My question is

  1. Should this compile?
  2. If it should, why?

I don’t think it should. There are many posts regarding undefined behaviour (UB) and enums, but I couldn’t find any that brought it up in a constant expression context. Also, most posts use an underlying type, I consider scoped enums without any fixed underlying type. Since I don’t have access to a copy of an ISO standard, I will refer to the latest C++17 draft found a cppreference.com here in the reasoning below.

First, we find the paragraph that discards expressions with UB from being constant expressions

[expr.const]/2: An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would evaluate one of the following expressions:

with subsection

[expr.const]/2.6: an operation that would have undefined behavior as specified in Clauses [intro] through [cpp] of this International Standard [ Note: including, for example, signed integer overflow (Clause [expr]), certain pointer arithmetic ([expr.add]), division by zero, or certain shift operations  — end note ] ;

which tells us that constant expressions may not contain UB.

Then, we find the section regarding static casts from int to enum

[expr.static.cast]/10: A value of integral or enumeration type can be explicitly converted to a complete enumeration type. The value is unchanged if the original value is within the range of the enumeration values ([dcl.enum]). Otherwise, the behavior is undefined. 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 ([conv.fpint]), and subsequently to the enumeration type.

which tells us that the result of a static cast is well defined if the operand is within the range of the target enum. The section refers to [decl.enum]/8 regarding the range of the enumeration values (too long to post here, I can't get the formatting right either). Anyways, it says that the range of valid values of an enum of non-fixed underlying type is defined by the smallest bitset that can fit all values between the smallest and the biggest enumeration (in two-complements format).

Finally, applying these three sections on the example code, we can say that the smallest bitfield that can contain both A = 0 and B = 1 is of size one. Hence, the integer 2 (which requires two bits to represent in two-complements format) is outside the range of the target enum E, and the static cast has undefined behaviour. Since a constexpr variable can’t have undefined behaviour, the program should not compile.

The closest I could get to the answer is in this post where they discuss whether the compiler is required to issue a diagnostic in the case of UB in constexpr: TLDR of that post is: Yes, but a warning might suffice to be complaint with the standard.

1 Answers

Scoped enums always have fixed underlying type. [dcl.enum]/5 (C++17):

For a scoped enumeration type, the underlying type is int if it is not explicitly specified. In both of these cases, the underlying type is said to be fixed.

So your E has fixed underlying type of int. Then in paragraph 8:

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

2 is in range for int, so by the text you quoted from [expr.static.cast], the behaviour of the cast is well-defined.

Related