2

Suppose the following simple example:

enum class Test { One = 1, Two = 2, Three = 3 };

int main()
{
    Test t{};
}

The code above seems to compile fine on Clang, GCC, and MSVC. My concern is while the underlying integral type used to represent the numeric range of the individual enumerators is perfectly capable of representing 0 as a value, that 0 value does not map to the constants themselves. It's this differentiation that is confusing (the integral type range vs the range of the enumeration's enumerators). t's value seems to be "well defined" from the perspective of an int, but it is not well defined in the sense that as a scalar, it does not map to one of the enums constants.

My searching resulted in this SO post, however the top answer was very technical and I didn't quite understand. I'm posing the question here from a slightly different angle, to see if I can get an answer that makes a little more sense to me.

Are there different rules and/or guarantees between enums with fixed value enumerators vs those without them? The code above makes perfect sense for enums with enumerators that have not been explicitly assigned, it's the fixed value case that is confusing.

Community
  • 1
  • 1
void.pointer
  • 24,859
  • 31
  • 132
  • 243
  • Objects of type enum have a range of valid values, which is dependent on the values of the enumerators. However, not all values in that range need to be associated with an enumerator; consider bit-flags where you'd OR some enumerators and convert the result back to the enumeration type. – dyp Oct 03 '15 at 15:59
  • @dyp do you have a small concrete example of the bit flags scenario you mentioned? – void.pointer Oct 03 '15 at 16:03
  • Hmm thinking a bit more about it, I wouldn't write code in such a way nowadays. I was thinking about `enum bit_flags { FLAG_A = 1 << 0, FLAG_B = 1 << 1 }; void foo(bit_flags); foo( bit_flags(FLAG_A | FLAG_B) );` There are better solutions such as separate `bit_flag` and `bit_set` types, where OR'ing multiple `bit_flag`s yields a `bit_set`. The `foo(bit_flags)` is, from a type safety point of view, probably still better than some `foo(int)`. – dyp Oct 03 '15 at 16:22

3 Answers3

1

The enumerators within an enumeration are used for three distinct purposes:

  1. They define names for values of this enumeration type.
  2. They shape the range of valid values for objects of the enumeration type, if the underlying type is not fixed.
  3. They influence the underlying type of the enumeration, if it is not fixed.

The range of valid values can include values for which there are no enumerators. This can of course be confusing, e.g. when you write a switch-statement that shall cover all values of the enumeration.


I'm just guessing, but the reason why there's not a 1-to-1 mapping between enumerators and valid enumeration values might be compatibility to C. In C, the enumerators have type int, hence the representation of the enumerators must be the int the enumerator stands for. You therefore can't map an enumerator A = 1 to a value 0. enums might have evolved from integer #defines.

Now, if you convert from int to the enumeration type, you shouldn't require a complicated program logic à la switch to map the enumerator values (ints) to the values stored in an object of the enumeration type. In fact, C requires that the enumeration type is compatible to some integer type. This implies that the representation of values of the enumeration type must be the same as the representation of the compatible integer type. As an example:

enum my_enum
{
    ENUMERATOR = 42
};

enum my_enum x = ENUMERATOR;

If my_enum is compatible to int, then the representation of the value stored in x must be the same as the representation of 42.

The compatibility requirement binds types so closely together that they can, for all I know, be used interchangeably:

enum my_enum
{
    ENUMERATOR_A = 0,
    ENUMERATOR_B = ~0
};

void foo(int);

int main() {
    foo(0);
}

void foo(enum my_enum x) {}

This is not considered an error by clang nor gcc, which use int as the compatible type for my_enum.

Because enumeration types are essentially typedefs for integer types, there can be values for which there are no enumerators.


C++ contains some hints about this relation between enumerations and integral types, such as [expr.static.cast] and [conv.integral] talking about not changing the value when converting between integral and enumeration types. However, I'm not sure if this is in and of itself meaningful, since I'm not sure how to observe that change except through a round-trip conversion.

There is no exception in C++'s strict aliasing rule between enumeration types and their underlying types.


Note that in C, we can exploit this relation to conveniently use bit-flags:

enum FLAGS
{
    FLAG_A = 1,
    FLAG_B = 2
};

void foo(enum FLAGS x);

int main() { foo(FLAG_A | FLAG_B); }

In C++, this is a type error: FLAG_A | FLAG_B is an int and not implicitly convertible to FLAGS. Although one can write an explicit cast such as foo( static_cast<FLAGS>(FLAG_A | FLAG_B) ), there are more convenient ways to write bit-sets in C++.


The zero-initialization part of the question probably is quite relevant as well: It is quite useful if it can be achieved by either a single memset or for static data, placing it in the .bss section. Therefore, having a value that is all bits 0 in types is quite useful.

One has to add though that pointers-to-data-members often have a null pointer value that is not represented by all bits 0, so it is not unheard of to break this useful property.

dyp
  • 38,334
  • 13
  • 112
  • 177
  • There's some additional information in *The Design and Evolution of C++* (Bjarne Stroustrup) as well as the C99 Rationale, but I think they can only serve as further citations / backup sources for this answer. The crucial piece that is still missing is the rationale for the weird bit-wise *valid range* rules in C++. – dyp Oct 03 '15 at 17:15
0

Summing up, the guarantees only refer to the range of possible values (minValue, maxValue) specified by the enum.

Enums without fixed values have the enumerator value guaranteed by the standard (§ 7.2/2)

An enum is guaranteed to hold any value in the range between it's smallest defined value and it's largest, and is unspecified on how it handles numbers outside the range. All the operations are made on its underlying type

Also from §5.2.9/10 we have rules about how to convert integrals to enum:

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

So converting an integer to a enum value outside the range gives unspecified value. This is a general rule, regardless if enumerators value have been specified in the declaration or not. However the range is given by the underlying type, which always can represent the xero, so it's not undefined (See relevant answer).

See Link to relevant question

Community
  • 1
  • 1
Lorenzo Belli
  • 1,767
  • 4
  • 25
  • 46
0

Are there different rules and/or guarantees between enums with fixed value enumerators vs those without them?

None. You should consider enums nothing more than type safe (type-safety only guaranteed in c++11 enum classes) integers.

KevinZ
  • 3,036
  • 1
  • 18
  • 26