C++: Cannot initialize enum value from a constant of the same type

1.6k views Asked by At

For unknown reason I cannot initialize enum value from a constexpr value. Here is my code:

enum class Enum: unsigned int; //Forward declaration

constexpr Enum constant = static_cast<Enum>(2);

enum class Enum: unsigned int {
  A = 0,
  B = 1,
  C = B,                                   //This works
  D = constant,                            //This FAILS
  E = static_cast<unsigned int>(constant), //This works
  F = Enum::B                              //This works
};

What I cannot understand is why I can write C = B, but cannot write D = constant (B and constant have the same type!)

I still can do E = static_cast<unsigned int>(constant), but it is too verbose (in my real-life code each enum value is initialized by a constexpr function call, and it is hard to put static_cast<unsigned int> everywhere).

2

There are 2 answers

4
dfrib On BEST ANSWER

All standard references below refers to N4659: March 2017 post-Kona working draft/C++17 DIS.


C++: Cannot initialize enum value from a constant of the same type

First of all, the enum type and its underlying type are not the same, and the enumerator of the former shall be defined with, if any, a constexpr value of the underlying type or a constant expression implicitly convertible to the underlying type.

As covered in the non-normative example of [dcl.enum]/10, there are no implicit conversions between a scoped enum and an integer, not even to its explicitly specified fixed underlying type:

The value of an enumerator or an object of an unscoped enumeration type is converted to an integer by integral promotion. [ Example: [...]

Note that this implicit enum to int conversion is not provided for a scoped enumeration:

enum class Col { red, yellow, green };
int x = Col::red;               // error: no Col to int conversion
Col y = Col::red;
if (y) { }                      // error: no Col to bool conversion

— end example ]

and, as governed by the non-presence of scoped enumerations in the normative text of [conv.integral]/1 and [conv.prom]/4 ([conv.prom]/3 for unscoped enumerations types whose underlying type is not fixed) [emphasis mine]:

[conv.integral]/1

A prvalue of an integer type can be converted to a prvalue of another integer type. A prvalue of an unscoped enumeration type can be converted to a prvalue of an integer type.

[conv.prom]/4

A prvalue of an unscoped enumeration type whose underlying type is fixed ([dcl.enum]) 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.

As such, your program, particularly the enumeration definition D = constant, is ill-formed.

And indeed, if we modify your example such that Enum is changed into a non-scoped enumeration, the program is no longer ill-formed.

enum Enum: unsigned int; //Forward declaration

constexpr Enum constant = static_cast<Enum>(2);

enum Enum: unsigned int {
  A = 0,
  B = 1,
  C = B,                                  // Ok
  D = constant,                           // Ok, implicit conversion
  E = static_cast<unsigned int>(constant) // Ok
};

int main() { }

Then why C = B in my code works?

Because of the somewhat tricky clause [dcl.enum]/5, which states that

Following the closing brace of an enum-specifier, each enumerator has the type of its enumeration. If the underlying type is fixed, the type of each enumerator prior to the closing brace is the underlying type [...]. If the underlying type is not fixed, the type of each enumerator prior to the closing brace is determined as follows:

  • [...]

In layman terms, the type of each enumerator basically changes depending on whether its viewed from within the definition of the enumeration or from outside of it.

This means that inside of the enum definition, we may use a previously defined enumerator in the definition of one that follows, as they both have the type (ignoring some details in [dcl.enum]/5.1 and [dcl.enum]/5.3 for enumerations where the underlying type is not fixed), no matter is the enum is scoped or not (i.e., no need for implicit conversions), whereas outside of the definition of the enum, these enumerators have the same type as that of the enum itself.

#include <type_traits>

enum class Enum: unsigned int; //Forward declaration

constexpr Enum constant = static_cast<Enum>(0);

// Forward a constexpr value whilst asserting
// type identity between two type template parameters.
template<typename T, typename U, unsigned int VALUE>
struct assert_and_get_value {
    static_assert(std::is_same_v<T, U>, "");
    static constexpr unsigned int value = VALUE;  
};

enum class Enum: unsigned int {
  A = 1,
  B,

  // C and B here are both of type 'unsigned int'
  C = B,
  D = assert_and_get_value<decltype(C), unsigned int, 5>::value,

  // Note that 'constant', however, in this scope, has type 'Enum'.
  E = assert_and_get_value<decltype(constant), const Enum, 6>::value,
};

// At this point, however, the type of each enumerator
// is the type of the enum.
static_assert(std::is_same_v<decltype(Enum::A), Enum>, "");
static_assert(!std::is_same_v<decltype(Enum::A), unsigned int>, "");

int main() {}
0
Łukasz Ślusarczyk On

It seems logical to me.

There are two kinds of entities here.

  1. Class being enum
  2. its internal type

B and C are both of the second kind so you can assign one to another. On the other hand D is of second kind and constant is of the first one. And enum class concept was designed to prevent implicit conversion from enum to its underlying type, thus your compilation error.