Are C++ enums signed or unsigned? And by extension is it safe to validate an input by checking that it is <= your max value, and leave out >= your min value (assuming you started at 0 and incremented by 1)?
Let's go to the source. Here's what the C++03 standard (ISO/IEC 14882:2003) document says in 7.2-5 (Enumeration declarations):
The underlying type of an enumeration
is an integral type that can represent
all the enumerator values defined in
the enumeration. It is
implementation-defined which integral
type is used as the underlying type
for an enumeration except that the
underlying type shall not be larger
than int unless the value of an
enumerator cannot fit in an int or
unsigned int.
In short, your compiler gets to choose (obviously, if you have negative numbers for some of your ennumeration values, it'll be signed).
You shouldn't rely on any specific representation. Read the following link. Also, the standard says that it is implementation-defined which integral type is used as the underlying type for an enum, except that it shall not be larger than int, unless some value cannot fit into int or an unsigned int.
In short: you cannot rely on an enum being either signed or unsigned.
You shouldn't depend on them being signed or unsigned. If you want to make them explicitly signed or unsigned, you can use the following:
enum X : signed int { ... }; // signed enum
enum Y : unsigned int { ... }; // unsigned enum
You shouldn't rely on it being either signed or unsigned. According to the standard it is implementation-defined which integral type is used as the underlying type for an enum. In most implementations, though, it is a signed integer.
In C++0x strongly typed enumerations will be added which will allow you to specify the type of an enum such as:
enum X : signed int { ... }; // signed enum
enum Y : unsigned int { ... }; // unsigned enum
Even now, though, some simple validation can be achieved by using the enum as a variable or parameter type like this:
enum Fruit { Apple, Banana };
enum Fruit fruitVariable = Banana; // Okay, Banana is a member of the Fruit enum
fruitVariable = 1; // Error, 1 is not a member of enum Fruit
// even though it has the same value as banana.
Even some old answers got 44 upvotes, I tend to disagree with all of them. In short, I don't think we should care about the underlying type of the enum.
First off, C++03 Enum type is a distinct type of its own having no concept of sign. Since from C++03 standard dcl.enum
7.2 Enumeration declarations
5 Each enumeration defines a type that is different from all other types....
So when we are talking about the sign of an enum type, say when comparing 2 enum operands using the < operator, we are actually talking about implicitly converting the enum type to some integral type. It is the sign of this integral type that matters. And when converting enum to integral type, this statement applies:
9 The value of an enumerator or an object of an enumeration type is converted to an integer by integral promotion (4.5).
And, apparently, the underlying type of the enum get nothing to do with the Integral Promotion. Since the standard defines Integral Promotion like this:
4.5 Integral promotions conv.prom
.. An rvalue of an enumeration type (7.2) can be converted to an rvalue of the first of the following types that can represent all the values of the enumeration
(i.e. the values in the range bmin to bmax as described in 7.2: int, unsigned int, long, or unsigned long.
So, whether an enum type becomes signed int or unsigned int depends on whether signed int can contain all the values of the defined enumerators, not the underlying type of the enum.
See my related question
Sign of C++ Enum Type Incorrect After Converting to Integral Type
In the future, with C++0x, strongly typed enumerations will be available and have several advantages (such as type-safety, explicit underlying types, or explicit scoping). With that you could be better assured of the sign of the type.
The compiler can decide whether or not enums are signed or unsigned.
Another method of validating enums is to use the enum itself as a variable type. For example:
enum Fruit
{
Apple = 0,
Banana,
Pineapple,
Orange,
Kumquat
};
enum Fruit fruitVariable = Banana; // Okay, Banana is a member of the Fruit enum
fruitVariable = 1; // Error, 1 is not a member of enum Fruit even though it has the same value as banana.
In addition to what others have already said about signed/unsigned, here's what the standard says about the range of an enumerated type:
7.2(6): "For an enumeration where e(min) is the smallest enumerator and e(max) is the largest, the values of the enumeration are the values of the underlying type in the range b(min) to b(max), where b(min) and b(max) are, respectively, the smallest and largest values of the smallest bitfield that can store e(min) and e(max). It is possible to define an enumeration that has values not defined by any of its enumerators."
So for example:
enum { A = 1, B = 4};
defines an enumerated type where e(min) is 1 and e(max) is 4. If the underlying type is signed int, then the smallest required bitfield has 4 bits, and if ints in your implementation are two's complement then the valid range of the enum is -8 to 7. If the underlying type is unsigned, then it has 3 bits and the range is 0 to 7. Check your compiler documentation if you care (for example if you want to cast integral values other than enumerators to the enumerated type, then you need to know whether the value is in the range of the enumeration or not - if not the resulting enum value is unspecified).
Whether those values are valid input to your function may be a different issue from whether they are valid values of the enumerated type. Your checking code is probably worried about the former rather than the latter, and so in this example should at least be checking >=A and <=B.
Check it with std::is_signed<std::underlying_type + scoped enums default to int
https://en.cppreference.com/w/cpp/language/enum implies:
main.cpp
#include <cassert>
#include <iostream>
#include <type_traits>
enum Unscoped {};
enum class ScopedDefault {};
enum class ScopedExplicit : long {};
int main() {
// Implementation defined, let's find out.
std::cout << std::is_signed<std::underlying_type<Unscoped>>() << std::endl;
// Guaranteed. Scoped defaults to int.
assert((std::is_same<std::underlying_type<ScopedDefault>::type, int>()));
// Guaranteed. We set it ourselves.
assert((std::is_same<std::underlying_type<ScopedExplicit>::type, long>()));
}
GitHub upstream.
Compile and run:
g++ -std=c++17 -Wall -Wextra -pedantic-errors -o main main.cpp
./main
Output:
0
Tested on Ubuntu 16.04, GCC 6.4.0.
While some of the above answers are arguably proper, they did not answer my practical question. The compiler (gcc 9.3.0) emitted warnings for:
enum FOO_STATUS {
STATUS_ERROR = (1 << 31)
};
The warning was issued on use:
unsigned status = foo_status_get();
if (STATUS_ERROR == status) {
(Aside from the fact this code is incorrect ... do not ask.)
When asked properly, the compiler does not emit an error.
enum FOO_STATUS {
STATUS_ERROR = (1U << 31)
};
Note that 1U makes the expression unsigned.
Related
This question already has an answer here:
What happens if you static_cast invalid value to enum class?
(1 answer)
Closed 7 years ago.
Say, we have
enum E
{
Foo = 0,
Bar = 1
};
Now, we do
enum E v = ( enum E ) 2;
And then
switch ( v )
{
case Foo:
doFoo();
break;
case Bar:
doBar();
break;
default:
// Is the compiler required to honor this?
doOther();
break;
}
Since the switch above handles every possible listed value of the enum, is it allowed for the compiler to optimize away the default branch above, or otherwise have an unspecified or undefined behavior in the case the value of enum is not in the list?
As I am expecting that the behavior should be similar for C and C++, the question is about both languages. However, if there's a difference between C and C++ for that case, it would be nice to know about it, too.
C++ situation
In C++, each enum has an underlying integral type. It can be fixed, if it is explicitly specified (ex: enum test2 : long { a,b};) or if it is int by default in the case of a scoped enum (ex: enum class test { a,b };):
[dcl.enum]/5: Each enumeration defines a type that is different from all other types. Each enumeration also has an underlying type. (...) if not
explicitly specified, the underlying type of a scoped enumeration type
is int. In these cases, the underlying type is said to be fixed.
In the case of an unscoped enum where the underlying type was not explicitely fixed (your example), the standard gives more flexibility to your compiler:
[dcl.enum]/7: For an enumeration whose underlying type is not fixed, the underlying type is an integral type that can represent all the
enumerator values defined in the enumeration. (...) It is implementation-defined which integral type is used as the underlying
type except that the underlying type shall not be larger than int
unless the value of an enumerator cannot fit in an int or unsigned
int.
Now a very tricky thing: the values that can be held by an enum variable depends on whether or not the underlying type is fixed:
if it's fixed, "the values of the enumeration are the values of the
underlying type."
otherwhise, it is the integral values within the minimum and the maximum of the smallest bit-field that can hold the smallest enumerator and the largest one.
You are in the second case, although your code will work on most compilers, the smallest bitfield has a size of 1 and so the only values that you can for sure hold on all compliant C++ compilers are those between 0 and 1...
Conclusion: If you want to ensure that the value can be set to 2, you either have to make your enum a scoped enum, or explicitly indicate an underlying type.**
More reading:
SO question on how to check if an enum value is valid
article on avoiding enum out-of-rang in secure coding.
Stroutstrup's plaidoyer for scoped enum over unscoped ones
C situation
The C situation is much simpler (C11):
6.2.5/16: An enumeration comprises a set of named integer constant values. Each distinct enumeration constitutes a different enumerated
type.
So basically, it is an int:
6.7.2.2./2 The expression that defines the value of an enumeration constant shall be an integer constant expression that has a value
representable as an int.
With the following restriction:
Each enumerated type shall be compatible with char, a signed integer
type, or an unsigned integer type. The choice of type is
implementation-defined, but shall be capable of representing the
values of all the members of the enumeration.
In C an enum type is an integer type large enough to hold all the enum constants:
(C11, 6.7.2.2p4) "Each enumerated type shall be compatible with char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined,110) but shall be capable of representing the values of all the members of the enumeration".
Let's say the selected type for enum E is _Bool. A _Bool object can only store the values 0 and 1. It's not possible to have a _Bool object storing a value different than 0 or 1 without invoking undefined behavior.
In that case the compiler is allowed to assume that an object of the enum E type can only hold 0 or 1 in a strictly conforming program and is so allowed to optimize out the default switch case.
C++Std 7.2.7 [dcl.enum]:
It is possible to define an enumeration that has values not defined by any of its enumerators.
So, you can have enumeration values which are not listed in enumerator list.
But in your specific case, the 'underlying type' is not 'fixed' (7.2.5). The specification doesn't say which is the underlying type in that case, but it must be integral. Since char is the smallest such type, we can conclude that there are other values of the enum which are not specified in the enumerator list.
Btw, I think that the compiler can optimize your case when it can determine that there are no other values ever assigned to v, which is safe, but I think there are no compilers which are that smart yet.
Also, 7.2/10:
An expression of arithmetic or enumeration type can be converted to an
enumeration type explicitly. The value is unchanged if it is in the
range of enumeration values of the enumeration type; otherwise the
resulting enumeration value is unspecified.
In C enumerators have type int . Thus any integer value can be assigned to an object of the enumeration type.
From the C Standard (6.7.2.2 Enumeration specifiers)
3 The identifiers in an enumerator list are declared as constants that
have type int and may appear wherever such are permitted.
In C++ enumerators have type of the enumeration that defines it. In C++ you should either expliicitly to specify the underlaying type or the compiler calculates itself the maximum allowed value.
From the C++ Standard (7.2 Enumeration declarations)
5 Each enumeration defines a type that is different from all other types. Each enumeration also has an underlying type. The underlying type can be explicitly specified using enum-base; if not explicitly specified, the underlying type of a scoped enumeration type is int. In these cases, the underlying type is said to be fixed. Following the closing brace of an enum-specifier, each enumerator has the type of its enumeration.
Thus in C any possible value of a enum is any integer value. The compiler may not optimize a switch removing the default label.
In C and C++, this can work.
Same code for both:
#include <stdio.h>
enum E
{
Foo = 0,
Bar = 1
};
int main()
{
enum E v = (enum E)2; // the cast is required for C++, but not for C
printf("v = %d\n", v);
switch (v) {
case Foo:
printf("got foo\n");
break;
case Bar:
printf("got bar\n");
break;
default:
printf("got \n", v);
break;
}
}
Same output for both:
v = 2
got default
In C, an enum is an integral type, so you can assign an integer value to it without casting. In C++, an enum is its own type.
I have some code where I need to do static_cast<foo>(bar) where foo is an integral type (say, simply, int) and bar is an enumeration value.
Is there a type that I can use for foo that guarantees that the static_cast is valid for any enumeration value for any enumerator?
Yes, that's std::underlying_type_t<decltype(bar)>.
If you need a common type for all enums, there's union { intmax_t s; uintmax_t u;}. Obviously, which of the two members to use in which case depends on the signed-ness of each individual enum type:
template<typename E>
std::enable_if<std::is_signed<std::underlying_type_t<E>>::value,
intmax_t>::type foo( // ...
There's nothing set out specific to enumerations in <cstdint>, but given that enumerations have to be integral types you could use
intmax_t
if your enumeration has negative values, or
uintmax_t
if there are no negative values.
These two types are defined as the widest integral types on your system. It's a pity you need to treat the unsigned and signed cases separately. There is no type that will work for all possibilities.
No, since there might be some enumeration types x and y where the underlying type of x is long long and that of y is unsigned long long.
There's no type in the standard that could fit all signed and unsigned long long values.
I am asking why the following code yields an error in Visual Studio 2014 update 4.
enum A
{ a = 0xFFFFFFFF };
enum class B
{ b = 0xFFFFFFFF };
I know that I can use enum class B : unsigned int. But why is the default underlying type of enum different that the default underlying type of enum class? There should be a design decision.
Clarifications
I forgot to mention the error:
error C3434: enumerator value '4294967295' cannot be represented as 'int', value is '-1'
That suggests that the default underlying type of enum class is signed int while the default type of enum is unsigned int. This question is about the sign part.
enum class is also called scoped enum.
enum is pretty much necessary for backwards compatibility reasons. scoped enum (or enum class) was added, among other reasons, to pin down the underlying type of the enum.
The details are as follows. When you do something like this:
enum MyEnumType {
Value1, Value2, Value3
};
The compiler is free to choose the underlying numeric type of MyEnumType as long as all your values can fit into that type. This means that the compiler is free to choose char, short, int, long, or another numeric type as the underlying type of MyEnumType. One practice that's done often is to add a last value to the enumeration to force a minimum size of the underlying type. For example:
enum MyEnumType2 {
Value1, Value2, Value3, LastValue=0xffffff
};
is guaranteed to have an underlying type of at least as large as unsigned 32-bit, but it could be larger (for example, 64-bit unsigned). This flexibility on the compiler's part is good and bad.
It is good in that you don't have to think about the underlying type. It is bad in that this is now an uncertainty that is up to the compiler, and if you do think about the underlying type, you can't do anything about it. This means that the same piece of code can mean different things on different compilers, which may, for example, be a problem if you wanted to do something like this:
MyEnumType a = ...;
fwrite(&a, sizeof(a), 1, fp);
Where you're writing the enum to a file. In this case, switching compiler or adding a new value to the enumeration can cause the file to be misaligned.
The new scoped enumeration solves this issue, among other things. In order to do this, when you declare a scoped enum, there must be a way for the language to fix the underlying type. The standard is, then, that:
enum class MyEnumType {
....
}
defaults to type int. The underlying type can be explicitly changed by deriving your enum class from the appropriate numeric type.
For example:
enum class MyEnumType : char {
....
}
changes the underlying type to char.
For this reason, default underlying type of an enum can change based on how many items and what literal values are assigned to the items in the enumeration. On the other hand, the default underlying type of an enum class is always int.
As far as N4140 is concerned, MSVC is correct:
§7.2/5 Each enumeration defines a type that is different from all
other types. Each enumeration also has an underlying type. The
underlying type can be explicitly specified using an enum-base. For
a scoped enumeration type, the underlying type is int if it is not
explicitly specified. [...]
For rationale, you can read the proposal entitled Strongly Typed Enums (Revision 3) N2347. Namely, section 2.2.2 Predictable/specifiable type (notably signedness) explains that the underlying type of enum is implementation-defined. For example, N4140 again:
§7.2/7 For an enumeration whose underlying type is not fixed, the
underlying type is an integral type that can represent all the
enumerator values defined in the enumeration. If no integral type can
represent all the enumerator values, the enumeration is ill-formed. It
is implementation-defined which integral type is used as the
underlying type except that the underlying type shall not be larger
than int unless the value of an enumerator cannot fit in an int or
unsigned int. If the enumerator-list is empty, the underlying type
is as if the enumeration had a single enumerator with value 0.
And N2347's proposed solutions:
This proposal is in two parts, following the EWG direction to date:
• provide a distinct new enum type having all the features that are
considered desirable:
o enumerators are in the scope of their enum
o enumerators and enums do not implicitly convert to int
o enums have a defined underlying type
• provide pure backward-compatible extensions for plain enums with a
subset of those features
o the ability to specify the underlying type
o the ability to qualify an enumerator with the name of the enum
The proposed syntax and wording for the distinct new enum type is
based on the C++/CLI [C++/CLI] syntax for this feature. The proposed
syntax for extensions to existing enums is designed for similarity.
So they went with the solution to give scoped enums a defined underlying type.
That's what the standard requires. A scoped enum always has an explicit
underlying type, which defaults to int unless you say otherwise.
As for the motivation: superficially, it doesn't make sense to conflate
the underlying type with whether the enum is scoped or not. I suspect
that this is done only because the authors want to always be able to
forward declare scoped enums; at least in theory, the size and
representation of a pointer to the enum may depend on the underlying
type. (The standard calls such forward declarations opaque enum types.)
And no, I don't think this is really a valid reason for conflating
scoping and underlying type. But I'm not the whole committee, and
presumably, a majority don't feel the way I do about it. I can't see
much use for specifying the underlying type unless you are forward
declaring the enum; it doesn't help with anything else. Where as I want
to use scoped enum pretty much everywhere I'm dealing with a real
enumeration. (Of course, a real enumeration will never have values
which won't fit in an int; those really only come up when you're using
an enum to define bitmasks.)
Warning:
src/BoardRep.h:49:12: warning: ‘BoardRep::BoardRep::Row::<anonymous struct>::a’
is too small to hold all values of ‘enum class BoardRep::Piece’
[enabled by default]
Piece a:2;
^
Enum:
enum class Piece: unsigned char {
EMPTY,
WHITE,
BLACK
};
Use:
union Row {
struct {
Piece a:2;
Piece b:2;
Piece c:2;
Piece d:2;
Piece e:2;
Piece f:2;
Piece g:2;
Piece h:2;
};
unsigned short raw;
};
With an enum I'd agree with GCC, it may have to truncate but that's because enums are not really separate from integers and pre-processor definitions. However an enum class is much stronger. If it is not strong enough to assume ALL Piece values taken as integers will be between 0 and 2 inclusive then the warning makes sense. Otherwise GCC is being needlessly picky and it might be worth mailing the list to say "look, this is a silly warning"
Incase anyone cannot see the point
You can store 4 distinct values in 2 bits of data, I only need 3 distinct values, so any enum of length 4 or less should fit nicely in the 2 bits given (and my enum does "derive" (better term?) from an unsigned type). If I had 5 or more THEN I'd expect a warning.
The warning issued by gcc is accurate, there's no need to compose a mail to the mailing list asking them to make the warning less likely to appear.
The standard says that an enumeration with the underlying type of unsigned char cannot be represented by a bitfield of length 2; even if there are no enumerations that holds such value.
THE STANDARD
The underlying value of an enumeration is valid even if there are no enum-keys corresponding to this value, the standard only says that a legal value to be stored inside an enumeration must fit inside the underlying type; it doesn't state that such value must be present among the enum-keys.
7.2 Enumeration declarations [dcl.enum]
7 ... It is possible to define an enumeration that has values not defined by any of its enumerators. ...
Note: the quoted section is present in both C++11, and the draft of C++14.
Note: wording stating the same thing, but using different terminology, can be found in C++03 under [dcl.enum]p6
Note: the entire [decl.enum]p7 hasn't been included to preserve space in this post.
DETAILS
enum class E : unsigned char { A, B, C };
E x = static_cast<E> (10);
Above we initialize x to store the value 10, even if there's no enumeration-key present in the enum-declaration of enum class E this is still a valid construct.
With the above in mind we easily deduce that 10 cannot be stored in a bit-field of length 2, so the warning by gcc is nothing but accurate.. we are potentially trying to store values in our bit-field that it cannot represent.
EXAMPLE
enum class E : unsigned char { A, B, C };
struct A {
E value : 2;
};
A val;
val.value = static_cast<E> (10); // OMG, OPS!?
According to the C++ Standard
8 For an enumeration whose underlying type is fixed, the values of the
enumeration are the values of the underlying type.
So the values of your enumeration are in the range
std::numeric_limits<unsigned char>::min() - std::numeric_limits<unsigned char>::max()
Bit field a defined as
Piece a:2;
can not hold all values of the enumeration.
If you would define an unscoped enumeration without a fixed underlying type then the range of its values would be
0 - 2
Yes, this warning is pointless because GCC already warns about assigning a value to a bitfield field (of enum type) that is truncated like this:
warning: conversion from 'Some_Enum' to 'unsigned char:2'
changes value from '(Some_Enum)9' to '1' [-Woverflow]
At the location of the warning it's only relevant that all declared enumerators can be held inside the bitfield field.
The statement that other values that are in the range of the underlying integer type (but don't correspond to a declared enumerator, which btw is well-defined, in general) can't be represented by the field, if ever assigned, is technically true, but has zero entropy, as a warning.
Thus, this warning was fixed in GCC 9.3.
IOW, GCC 9.3 and later don't warn about such code, anymore.
Suppose I have (on a 32 bit machine)
enum foo {
val1 = 0x7FFFFFFF, // originally '2^31 - 1'
val2,
val3 = 0xFFFFFFFF, // originally '2^32 - 1'
val4,
val5
};
what is the value of val2, val4 and val5? I know I could test it, but is the result standardized?
In C standard:
C11 (n1570), § 6.7.2.2 Enumeration specifiers
Each enumerated type shall be compatible with char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined, but shall be capable of representing the values of all the members of the enumeration.
If the underlying type used by the compiler is not capable to represent these values, the behavior is undefined.
C11 (n1570), § 4. Conformance
If a ‘‘shall’’ or ‘‘shall not’’ requirement that appears outside of a constraint or runtime-constraint is violated, the behavior is undefined.
From the C++11 standard (§7.2,6, emphasis mine):
For an enumeration whose underlying type is not fixed, the underlying type is an integral type that can represent all the enumerator values defined in the enumeration. If no integral type can represent all the enumerator values, the enumeration is ill-formed. It is implementation-defined which integral type is used as the underlying type except that the underlying type shall not be larger than int unless the value of an enumerator cannot fit in an int or unsigned int.
So the compiler will happily do The Right Thing if there is an integral type bigger than 32bit. If not, the enum is illformed. There will be no wrapping around.
The values will be:
enum foo {
val1 = 0x7FFFFFFF,
val2, // 0x80000000 = 2^31
val3 = 0xFFFFFFFF,
val4, //0x0000000100000000 = 2^32
val5 //0x0000000100000001 = 2^32+1
};
The increasing numbers are well defined as well (§7.2,2):
[...] An enumerator-definition without an initializer gives the enumerator the value obtained by increasing the value of the previous enumerator by one.
C99 / C11
Prelude:
5.2.4.2.1 requires int to be at least 16 bits wide; AFAIK there's no upper bound (long must be longer or equal, though, 6.2.5 /8).
6.5 /5:
If an exceptional condition occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not in the range of representable values for its type), the behavior is undefined.
If your `int` is 32 bits wide (or less)
then the example in the OP is a violation of constraint 6.7.2.2 /2:
The expression that defines the value of an enumeration constant shall be an integer
constant expression that has a value representable as an int.
Furthermore, the enumerators are defined as constant of type int, 6.7.2.2 /3:
The identifiers in an enumerator list are declared as constants that have type int and
may appear wherever such are permitted.
Note, there's a difference between the type of the enumeration and the type of an enumerator / enumeration constant:
enum foo { val0 };
enum foo myVariable; // myVariable has the type of the enumeration
uint_least8_t v = val0*'c'; // if val0 appears in any expression, it has type int
It seems to me this allows narrowing, e.g. reducing the size of the enum type to 8 bits:
enum foo { val1 = 1, val2 = 5 };
enum foo myVariable = val1; // allowed to be 8-bit
But it seems to disallow widening, e.g.
enum foo { val1 = INT_MAX+1 }; // constraint violation AND undefined behaviour
// not sure about the following, we're already in UB-land
enum foo myVariable = val1; // maximum value of an enumerator still is INT_MAX
// therefore myVariable will have sizeof int
Auto-increment of enumerators
Because of 6.7.2.2 /3,
[...] Each subsequent enumerator with no = defines its enumeration constant as the value of the constant expression obtained by adding 1 to the value of the previous enumeration constant. [...]
the example results in UB:
enum foo {
val0 = INT_MAX,
val1 // equivalent to `val1 = INT_MAX+1`
};
Here's the C++ answer: in 7.2/6, it states:
[...] the underlying type is an integral type that can represent all
the enumerator values defined in the enumeration. If no integral type
can represent all the enumerator values, the enumeration is
ill-formed. It is implementation-defined which integral type is used
as the underlying type except that the underlying type shall not be
larger than int unless the value of an enumerator cannot fit in an int
or unsigned int.
So compared to C: no undefined behavior if the compiler can't find a type, and the compiler can't just use its 512-bit extended integer type for your two-value enum.
Which means that in your example, the underlying type will probably be some signed 64-bit type - most compilers always try the signed version of a type first.