5

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.

Hector
  • 2,464
  • 3
  • 19
  • 34
  • Because they are two different things? The "design decision" was to create `enum class`. Not sure what you're after here. – Lightness Races in Orbit Feb 22 '15 at 16:12
  • 1
    There is no default type of enum. It can change by compiler. Sometimes people put, add LAST=0xffffffff to force 32 bit unsigned underlying type. However, that doesn't guarantee anything because the compiler is free to make it 64-bit signed (or unsigned) or some other type. – thang Feb 22 '15 at 16:22
  • 1
    @Hector see http://stackoverflow.com/a/8357462/3093378 in case you want to extract automatically the underlying type using `std::underlying_type` – vsoftco Feb 22 '15 at 16:39

3 Answers3

6

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.

thang
  • 3,466
  • 1
  • 19
  • 31
  • 3
    Specifying the underlying type is not limited to scoped enums. You can do the same thing with unscoped ones. You **can** say `enum MyEnumType : int` and get an unscoped enum with a fixed underlying type. – bogdan Feb 22 '15 at 16:56
  • @bogdan, did not know that. i removed that note. – thang Feb 22 '15 at 16:57
  • Of course, even when you specify the underlying type (or it is implicitly specified), something like `fwrite( &a, sizeof(a), 1, fp )` won't output anything useful, that can be read from another program. – James Kanze Feb 22 '15 at 16:59
  • And of course, scoped enums weren't added to pin down the underlying type; they were added to change the scope of the enum constants. Why the standard committee also forced them to have a specific underlying type is beyond me. – James Kanze Feb 22 '15 at 17:00
  • @JamesKanze, that's true, but in the next version of the code (same program), if someone adds another enum, it won't cause a problem. Also, if you switch compiler or platform... There's a list of several reasons why scoped enum was added. well-defined underlying type is one reason. Namely: underlying type, type safety, scope issues, forward declaration. I think that's all as far as I can remember. The other reasons aren't relevant here. – thang Feb 22 '15 at 17:00
  • @thang It's irrelevant whether someone adds a new value or not: the use of `fwrite` to write an enum is not portable, and can only result is something which you may not be able to read in the future (or even on a different machine). The new enums provide type safety and scope; the explicit underlying type provides an explicit underlying type. Those are two separate issues, which the committee (IMHO mistakenly) decided to conflate. – James Kanze Feb 22 '15 at 17:05
  • @JamesKanze, not portable how? it's portable as long as the same program can write and read its data. As long as the meaning stays the same between fwrite and fread, there is no issue. It's much better to fwrite an enum with fixed underlying type than to write an int with corresponding #define. Scoped enum provides all the things I listed in my previous comment. Please look it up. I guess it could just as well be called "forward-declarable enum". I guess the committee decided that scoped values is most important. – thang Feb 22 '15 at 17:08
  • @thang It's portable as long as you don't port anything? Not even to the next version of the compiler? The size and representation of an `int` can and do vary from one system to the next; I've even seen the representation of a `long` vary from one version of a compiler to the next. (And the compiler which did that was Microsoft C. Not exactly an exotic.) – James Kanze Feb 22 '15 at 17:13
  • @JamesKanze, size of int, while not specifically pinned down, is 4 bytes for most common platforms except for old (or microcontroller, DSP, etc) 16 and 8 bit systems. I suspect this is not done coincidentally. The more common use case is if someone goes back and adds more values to the enum. Changing platform from 32 to 64-bit for example, where sizeof(int) may change, probably will require some porting effort anyway. Of course, for good portability, the underlying type should be fixed to something like int32_t (or a type with guaranteed size). – thang Feb 22 '15 at 17:26
  • I'm aware of `int` of 16, 32, 36 and 48 bits. And even where `int` is 32 bits, it doesn't necessarily have the same representation; if you write one value, you may very well read another if you read on a different machine. (And why would a change in the size of `int` require _any_ porting? What sort of code depends on the actual size of `int`, as opposed to a minimum size?) – James Kanze Feb 23 '15 at 18:03
  • @thang And `int32_t` isn't available everywhere, and even where it is, it doesn't solve the problem. – James Kanze Feb 23 '15 at 18:04
3

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.

1

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

James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • "I can't see much use for specifying the underlying type unless you are forward declaring the enum" … But that alone is a very good reason in my opinion (I am just a newbie thou). If the enum has 100+ entries, I would prefer to have those entries in a different header file. – Hector Feb 23 '15 at 14:22