The enumerators within an enumeration are used for three distinct purposes:
- They define names for values of this enumeration type.
- They shape the range of valid values for objects of the enumeration type, if the underlying type is not fixed.
- 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
. enum
s might have evolved from integer #define
s.
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 (int
s) 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.