Home | Projects | Notes > C++ Programming > Enumerations
What is an enumeration?
Motivation
Structure of an enumeration
Types of enumerations
Unscoped enumeration
Scoped enumeration
Enumerations in use
A user-defined type that models a set of constant integral values.
The days of the week (Mon, Tue, Wed, ...)
The months of the year (Jan, Feb, Mar, ...)
The suits in a deck of cards (Clubs, Hearts, Spades, Diamonds)
The values in a deck of cards (Ace, Two, Three, ...)
States of a system (Idle, Defense_Mode, Attack_Mode, ...)
The directions on a compass (North, South, East, West)
Prior to enumerated types many algorithms contained unnamed numerical constants (a.k.a. "Magic numbers"). These constants are unique values with often no explanation as to how they are obtained. To make matters worse, these constants were often used as conditionals in control statements leaving one with no idea what an algorithm was doing. This often resulted in many algorithms suffering from low readability and high numbers of logic errors.
Therefore, the two important motivations for introducing the concept of enumeration were
To improve readability
To improve the algorithm correctness
Improving readability of the code
xxxxxxxxxx
91int state;
2std::cin >> state;
3
4if (state == 0)
5 initiate(3);
6else if (state == 1)
7 initiate(4);
8else if (state == 2)
9 initiate(5);
Poor readability since the meaning of values used in the code is not self-explanatory.
Above code snippet can be improved by using enumerated types.
xxxxxxxxxx
131enum State {Engin_Failure = 0, Inclement_Weather = 1, Nominal = 2};
2enum Sequence {Abort = 3, Hold = 4, Launch = 5};
3
4int user_input;
5std::cin >> user_input;
6State state = State(user_input);
7
8if (state == Engin_Failure) // state = 0
9 initiate(Abort); // sequence = 3
10else if (state == Inclement_Weather) // state = 1
11 initiate(Hold); // sequence = 4
12else if (state == Nominal) // state = 2
13 initiate(Launch); // sequence 5
Improved readability!
Improving algorithm correctness
xxxxxxxxxx
91int get_state()
2{
3 return state_of_fridge;
4}
5
6int getState()
7{
8 return state_of_rocket;
9}
xxxxxxxxxx
81int state = get_state();
2
3 if (state == 0)
4 initiate(3);
5else if (state == 1)
6 initiate(4);
7else if (state == 2)
8 initiate(5);
In this scenario, even though you used the wrong state getter function, the code will still compile because both of the getter functions return
int
values.We don't want the launch sequence to be determined by the state of the refrigerator.
By using enumerated types, we can impose a type restriction on the state
variable.
xxxxxxxxxx
81State state = get_state();
2
3if (state == Engin_Failure)
4 initiate(Abort);
5else if (state == Inclement_Weather)
6 initiate(Hold);
7else if (state == Nominal)
8 initiate(Launch);
Now, the variable
state
can only be one of the three predefined values of the enumerated typeState
. If we try to assign a value to thestate
variable that is not one of these three values, the compiler will generate an error! This means that if the programmer happens to call the wrong state getter function, the program will not compile.
Syntax
xxxxxxxxxx
11enum-key enum-name : enumerator-type { };
enum-key
- Defines the beginning of the enumeration as well as the scope (Regardless of the type of enumeration, scoped or unscoped, an enumeration always begins with theenum
keyword.)
enum-name
- Optional name of the enumeration
enumerator-type
- Enumerator type; can be omitted and the compiler will try to deduce it
{ }
- Enumerator list; list of enumerator definitions (names and integral values associated with them)
Enumerator initialization
Simplest form - no name, no fixed underlying type
xxxxxxxxxx
21enum {Red, Green, Blue}; // Implicit initialization
2// 0 1 2
Unless specified otherwise, the compiler will assign 0 to the first element, and then assign the value of 'previous enumerator + 1' to all the subsequent elements. (Implicit initialization)
xxxxxxxxxx
11enum {Red = 1, Green = 2, Blue = 3}; // Explicit initialization
xxxxxxxxxx
21enum {Red = 1, Green, Blue}; // Explicit/Implicit initialization
2// 2 3
Enumerator type
Integral Type | Width in Bits |
---|---|
int | at least 16 |
unsigned int | at least 16 |
long | at least 32 |
unsigned long | at least 32 |
long long | at least 64 |
unsigned long long | at least 64 |
If the underlying type of enumeration is not fixed, the compiler assigns the first integral type that's able to hold the enumeration's entire value range.
xxxxxxxxxx
71enum {Red, Green, Blue}; // Underlying type: int
2// 0 1 2
3// 000 001 010
4
5enum {Red, Green, Blue = -32769}; // Underlying type: long
6// 0 1 -32769
7// 000 001 11111111111111110111111111111111
In some cases, we may want to fix an enumeration's underlying type for the purpose of saving memory or increasing the accuracy of calculations involving numerator values.
xxxxxxxxxx
21enum : uint8_t {Red, Green, Blue}; // Underlying type: unsigned 8 bit int
2enum : long long {Red, Green, Blue}; // Underlying type: long long
Enumeration name
xxxxxxxxxx
71enum {Red, Green, Blue}; // Anonymous; No type safety (Not possible to declare variables
2 // of that enumeration type.)
3
4int my_color;
5
6my_color = Green; // Valid
7my_color = 4; // Also valid
No type safety for anonymous enumeration!
xxxxxxxxxx
71enum Color {Red, Green, Blue}; // Named; Type safe
2
3Color my_color; // It is assumed that 'my_color' can only have one of the three values;
4 // Red, Green or Blue.
5
6my_color = Green; // Valid
7my_color = 4; // Not valid
With the enumeration name
Color
, it is now type safe!
Enumerations whose enumerators are NOT qualified and are visible throughout the entire scope in which they declared.
Syntax
xxxxxxxxxx
11enum enum-name : enumerator-type { };
enum
- Enum; Defines an unscoped enumeration
Using if
and switch
statements with unscoped enumerations
xxxxxxxxxx
81State state = get_state();
2
3if (state == Nominal) // Accessed first
4 initiate(Launch);
5else if (state == Inclement_Weather) // Accessed second
6 initiate(Hold);
7else if (state == Engine_Failure) // Accessed last
8 initiate(Abort);
This control structure may not be the best implementation. Consider a situation where the current state is
Engine_Failure
and the system needs to initiateAbort
sequence ASAP. With this control structure, you'll need to go through each and every conditional until it finally reachesEngine_Failure
.
switch
statements are by far the most popular control structure used with enumerations. (Better readability and access time than if...else
statements in general.)
xxxxxxxxxx
141State state = get_state();
2
3switch (state)
4{
5 case Engine_Failure: // Equal access time
6 initiate(Abort);
7 break;
8 case Inclement_Weather: // Equal access time
9 initiate(Hold);
10 break;
11 case Nominal: // Equal access time
12 initiate(Launch);
13 break;
14}
Note that each label within a
switch
statement must be unique!
Using cin
with unscoped enumerations
xxxxxxxxxx
61enum State {Engine_Failure, Inclement_Weather, Nominal};
2
3State state;
4std::cin >> state; // NOT allowed using standard extraction operator (This is because the
5 // extraction operator `>>` has no knowledge of what a `State` type
6 // variable is or how to deal with it.
One solution is to extract the user input value into a type that the extraction operator >>
does know about, and then cast it to the State
type variable.
xxxxxxxxxx
51enum State {Engine_Failure, Inclement_Weather, Nominal}; // Underlying type: int
2 0 1 2
3std::underlying_type_t<State> user_input; // Type: int
4std::cin >> user_input; // User input = 3
5State state = State(user_input); // state = 3
When we declare the type of temporary variable, in some cases, the underlying type of an enumeration might not be obvious. So we can use the class template
std::underlying_type_t
to deduce it. In this case, the underlying type of theState
enumeration is integer, so the type of theuser_input
variable will also be integer.It's important to note that when we cast a variable to an enumeration types, the value of the variable does not change. This means that there's no guarantee that the casted enumeration type variable will have the value corresponding to the enumeration's enumerators. For example, if the user were to enter the value 3, it would be cast to the
State
type variablestate
with no issues. Obviously, this is not what we want since 3 does not correspond to anyState
enumerator.One way to workaround this situation is to implement our own checks to ensure that the variable we are casting has a value corresponding to one of the enumeration's enumerators. This is usually done by using the
switch
statement with the condition being the variable to be casted (e.g.,user_input
in this case), and the cases being each of the enumeration's enumerator.There can be many other ways to do the checks, but the key is that it is your responsibility to perform the checks!
xxxxxxxxxx
121enum State {Engine_Failure, Inclement_Weather, Nominal}; // Underlying type: int
2 0 1 2
3std::underlying_type_t<State> user_input; // Type: int
4std::cin >> user_input; // User input = 3
5State state;
6switch (user_input)
7{
8 case Engine_Failure: state = State(user_input); break;
9 case Inclement_Weather: state = State(user_input); break;
10 case Nominal: state = State(user_input); break;
11 default: std::cout << "User input is not a valid state.";
12}
Another solution is to overload the extraction operator >>
to deal with the State
type variables.
xxxxxxxxxx
151enum State {Engine_Failure, Inclement_Weather, Nominal}; // Underlying type: int
2 0 1 2
3std::istream& operator>>(std::istream& is, State& state)
4{
5 std::underlying_typt_t<State> user_input; // Type: int
6 is >> user_input;
7 switch (user_input)
8 {
9 case Engine_Failure: state = State(user_input); break;
10 case Inclement_Weather: state = State(user_input); break;
11 case Nominal: state = State(user_input); break;
12 default: std::cout << "User input is not a valid state.";
13 }
14 return is; // Input stream is returned to the calling function
15}
With the overloaded extraction operator, the following code becomes not only valid, but also more clean and readable.
xxxxxxxxxx
21State state;
2std::cin >> state; // Valid with overloaded extraction operator
Using cout
with unscoped enumerations to display enumeration-type variables
xxxxxxxxxx
31enum State {Engine_Failure, Inclement_Weather, Nominal};
2State state = Engine_Failure;
3std::cout << state; // Displays 0
In this case, the enumeration-type variable is implicitly converted to its underlying type, and its value is displayed. (Note that it's their value that gets displayed, not the enumerator's name! This makes sense, because an enumerator's name isn't a string, but is simply a name used to identify its value within the code.)
Sometimes, it may be useful to display the name of the enumeration type variable. To accomplish this, we can use switch
statement.
xxxxxxxxxx
91enum State {Engine_Failure, Inclement_Weather, Nominal};
2State state = Engine_Failure;
3switch (state)
4{
5 case Engine_Failure: std::cout << "Engine Failure"; break;
6 case Inclement_Weather: std::cout << "Inclement Weather"; break;
7 case Nominal: std::cout << "Nominal"; break;
8 default: std::cout << "Unknown";
9} // Displays "Engine Failure"
Another way to accomplish this is to overload the insertion operator <<
.
xxxxxxxxxx
131enum State {Engine_Failure, Inclement_Weather, Nominal};
2
3std::ostream& operator<<(std::ostream& os, const State& state)
4{
5 switch (state)
6 {
7 case Engine_Failure: os << "Engine Failure";
8 case Inclement_Weather: os << "Inclement Weather";
9 case Nominal: os << "Nominal";
10 default: os << "Unknown";
11 }
12 return os; // Output stream is returned to the calling function
13}
L3: Note that the second parameter type must be the
const
reference!
By overloading the insertion operator, we can write a clean and readable code as follows.
xxxxxxxxxx
21State state = Engine_Failure;
2std::cout << state; // Displays "Engine Failure"
Enumerations whose enumerators are qualified, and therefore they are only visible by using the scope resolution operator ::
. (Everything you can do with unscoped enumerations, you can do with scoped enumerations.)
Syntax
xxxxxxxxxx
11enum class enum-name : enumerator-type { };
enum class
(orenum struct
) - Defines a scoped enumeration
Unscoped enumerations can sometimes cause issues that can only be resolved by using scoped enumerations.
Example 1:
xxxxxxxxxx
51enum Whale {Blue, Beluga, Gray};
2enum Shark {Greatwhite, Gammerhead, Bull};
3
4if (Beluga == Hammerhead)
5 std::cout << "A beluga whale is equivalent to a hammerhead shark.";
Saying that
Beluga
whale andHammerhead
shark are equivalent just because they represent the same value 1 does not make sense!
Example 2: Name clashes!
xxxxxxxxxx
21enum Whale {Blue, Beluga, Gray};
2enum Shark {Greatwhite, Gammerhead, Bull, Blue}; // Error: Blue already defined
We don't want to have to worry about name clashes when we are naming enumerators.
Scoped enumerations solve these issues!
Using if
and switch
statements with scoped enumerations (Identical to that of unscoped, but with the additional requirement of specifying enumerator scopes)
xxxxxxxxxx
91enum class Whale {Blue, Beluga, Gray};
2Whale whale = Whale::Beluga;
3
4if (whale == Whale::Blue)
5 std::cout << "Blue whale";
6else if (whale == Whale::Beluga)
7 std::cout << "Beluga whale";
8else if (whale == Whale::Gray)
9 std::cout << "Gray whale";
xxxxxxxxxx
151enum class Whale {Blue, Beluga, Gray};
2Whale whale = Whale::Beluga;
3
4switch (whale)
5{
6 case Whale::Blue:
7 std::cout << "Blue whale";
8 break;
9 case Whale::Beluga:
10 std::cout << "Beluga whale";
11 break;
12 case Whale::Gray:
13 std::cout << "Gray whale";
14 break;
15}
Using scoped enumerator values
Unlike the unscoped enumerations, compiler will not deduce and convert the enumerator values of scoped enumerations. (Scoped enumeration type variables are not implicitly convertable! Instead, we must explicitly cast the variable to its underlying type.)
xxxxxxxxxx
51enum class Item {Milk = 350, Bread = 250, Apple = 132}; // Underlying type: int
2
3int milk_code = Item::Milk; // Error: Cannot conver Item to int
4int total = Item::Milk + Item::Bread; // Error: No matching '+' operator
5std::cout << Item::Milk; // Error: No matching '<<' operator
xxxxxxxxxx
81enum class Item {Milk = 350, Bread = 250, Apple = 132}; // Underlying type: int
2
3int milk_code = int(Item::Milk); // milk_code = 350
4// Or
5int milk_code = static_cast<int>(Item::Milk);
6
7int total = int(Item::Milk) + int(Item::Bread); // total = 600
8std::cout << underlying_type_t<Item>(Item::Milk); // Displays 350
L3, L5: Only after appropriate type casting (there can be a few different styles of casting), can we use the value of scoped enumeration type variable.