Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Converting Enumerations to Integers
Convert enumerators to and from integers for storage and arithmetic operations.
In the prior lesson, we mentioned that enumerators are symbolic constants. What we didn't tell you then is that these enumerators have values that are of an integral type.
This is similar to the case with chars. Consider:
char letter{ 'Z' };
A char is really just a 1-byte integral value, and the character 'Z' gets converted to an integral value (in this case, 90) and stored.
When we define an enumeration, each enumerator is automatically associated with an integer value based on its position in the enumerator list. By default, the first enumerator is given the integral value 0, and each subsequent enumerator has a value one greater than the previous enumerator:
enum Difficulty
{
beginner, // 0
intermediate, // 1
advanced, // 2
expert, // 3
legendary, // 4
};
int main()
{
Difficulty level{ advanced }; // level actually stores integral value 2
return 0;
}
It is possible to explicitly define the value of enumerators. These integral values can be positive or negative, and can share the same value as other enumerators. Any non-defined enumerators are given a value one greater than the previous enumerator.
enum Temperature
{
freezing = -10, // values can be negative
cold, // -9
cool, // -8
warm = 5,
hot = 5, // shares same value as warm
scorching, // 6
};
Note in this case, warm and hot have been given the same value. When this happens, the enumerators become non-distinct—essentially, warm and hot are interchangeable. Although C++ allows it, assigning the same value to two enumerators in the same enumeration should generally be avoided.
Most of the time, the default values for enumerators will be exactly what you want, so do not provide your own values unless you have a specific reason to do so.
Avoid assigning explicit values to your enumerators unless you have a compelling reason to do so.
If an enumeration is zero-initialized (which happens when we use value-initialization), the enumeration will be given value 0, even if there is no corresponding enumerator with that value.
#include <iostream>
enum Temperature
{
freezing = -10, // -10
cold, // -9
cool, // -8
// note: no enumerator with value 0 in this list
warm = 5, // 5
hot = 5, // 5
scorching, // 6
};
int main()
{
Temperature temp{}; // value-initialization zero-initializes temp to value 0
std::cout << temp; // prints 0
return 0;
}
This has two semantic consequences:
- If there is an enumerator with value 0, value-initialization defaults the enumeration to the meaning of that enumerator. For example, using the prior
enum Difficultyexample, a value-initializedDifficultywill default tobeginner). For this reason, it is a good idea to consider making the enumerator with value 0 the one that represents the best default meaning for your enumeration.
Something like this is likely to cause problems:
enum ValidationResult
{
invalid, // default value (0)
valid
};
- If there is no enumerator with value 0, value-initialization makes it easy to create a semantically invalid enumeration. In such cases, we recommend adding an "invalid" or "unknown" enumerator with value 0 so that you have documentation for the meaning of that state, and a name for that state that you can explicitly handle.
enum MatchResult
{
resultUnknown, // default value (0)
team1Won,
team2Won,
};
// somewhere later in your code
if (result == resultUnknown) // handle case appropriately
Make the enumerator representing 0 the one that is the best default meaning for your enumeration. If no good default meaning exists, consider adding an "invalid" or "unknown" enumerator that has value 0, so that state is explicitly documented and can be explicitly handled where appropriate.
Even though enumerations store integral values, they are not considered to be an integral type (they are a compound type). However, an unscoped enumeration will implicitly convert to an integral value. Because enumerators are compile-time constants, this is a constexpr conversion.
Consider the following program:
#include <iostream>
enum Difficulty
{
beginner, // assigned 0
intermediate, // assigned 1
advanced, // assigned 2
expert, // assigned 3
legendary, // assigned 4
};
int main()
{
Difficulty level{ advanced };
std::cout << "Your difficulty level is " << level << '\n'; // what does this do?
return 0;
}
Since enumerated types hold integral values, as you might expect, this prints:
Your difficulty level is 2
When an enumerated type is used in a function call or with an operator, the compiler will first try to find a function or operator that matches the enumerated type. For example, when the compiler tries to compile std::cout << level, the compiler will first look to see if operator<< knows how to print an object of type Difficulty (because level is of type Difficulty) to std::cout. It doesn't.
Since the compiler can't find a match, it will then then check if operator<< knows how to print an object of the integral type that the unscoped enumeration converts to. Since it does, the value in level gets converted to an integral value and printed as integral value 2.
We show how to convert an enumeration into a string in a later lesson. We teach `std::cout` how to print an enumerator in another upcoming lesson.
Enumerators have values that are of an integral type. But what integral type? The specific integral type used to represent the value of enumerators is called the enumeration's underlying type (or base).
For unscoped enumerations, the C++ standard does not specify which specific integral type should be used as the underlying type, so the choice is implementation-defined. Most compilers will use int as the underlying type (meaning an unscoped enum will be the same size as an int), unless a larger type is required to store the enumerator values. But you shouldn't assume this will hold true for every compiler or platform.
It is possible to explicitly specify an underlying type for an enumeration. The underlying type must be an integral type. For example, if you are working in some bandwidth-sensitive context (e.g. sending data over a network) you may want to specify a smaller type for your enumeration:
#include <cstdint> // for std::int8_t
#include <iostream>
// Use an 8-bit integer as the enum underlying type
enum Difficulty : std::int8_t
{
beginner,
intermediate,
advanced,
};
int main()
{
Difficulty level{ beginner };
std::cout << sizeof(level) << '\n'; // prints 1 (byte)
return 0;
}
Specify the base type of an enumeration only when necessary.
Because `std::int8_t` and `std::uint8_t` are usually type aliases for char types, using either of these types as the enum base will most likely cause the enumerators to print as char values rather than int values.
While the compiler will implicitly convert an unscoped enumeration to an integer, it will not implicitly convert an integer to an unscoped enumeration. The following will produce a compiler error:
enum Direction // no specified base
{
north, // assigned 0
south, // assigned 1
east, // assigned 2
west, // assigned 3
};
int main()
{
Direction heading{ 2 }; // compile error: integer value 2 won't implicitly convert to a Direction
heading = 3; // compile error: integer value 3 won't implicitly convert to a Direction
return 0;
}
There are two ways to work around this.
First, you can explicitly convert an integer to an unscoped enumerator using static_cast:
enum Direction // no specified base
{
north, // assigned 0
south, // assigned 1
east, // assigned 2
west, // assigned 3
};
int main()
{
Direction heading{ static_cast<Direction>(2) }; // convert integer 2 to a Direction
heading = static_cast<Direction>(3); // heading changed from east to west!
return 0;
}
We'll see an example in a later lesson where we make use of this.
It is safe to static_cast any integral value that is represented by an enumerator of the target enumeration. Since our Direction enumeration has enumerators with values 0, 1, 2, and 3, static_casting integral values 0, 1, 2, and 3 to a Direction is valid.
It is also safe to static_cast any integral value that is in range of the target enumeration's underlying type, even if there are no enumerators representing that value. Static casting a value outside the range of the underlying type will result in undefined behavior.
If the enumeration has an explicitly defined underlying type, the range of the enumeration is identical to the range of the underlying type.
If the enumeration does not have an explicit underlying type, things are a bit more complicated. In this case, the compiler gets to pick the underlying type, and it can pick any signed or unsigned type so long as the value of all enumerators fit in that type. Given this, it is only safe to static_cast integral values that fit in the range of the smallest number of bits that can hold the value of all enumerators.
Let's do two examples to illustrate this:
- With enumerators that have values 1, 7, and 15, these enumerators could minimally fit in an unsigned 4-bit integral type with range 0 to 15. Therefore, it is only safe to static_cast integral values 0 through 15 to this enumerated type.
- With enumerators that have values -20, 5, and 10, these enumerators could minimally fit in a signed 6-bit integral type with range -32 to 31. Therefore, it is only safe to static_cast integral values -32 through 31 to this enumerated type.
Second, as of C++17, if an unscoped enumeration has an explicitly specified base, then the compiler will allow you to list initialize an unscoped enumeration using an integral value:
int main() { Direction heading1{ 2 }; // ok: can brace initialize unscoped enumeration with specified base with integer (C++17) Direction heading2(2); // compile error: cannot direct initialize with integer Direction heading3 = 2; // compile error: cannot copy initialize with integer
heading1 = 3; // compile error: cannot assign with integer
return 0;
}
Converting Enumerations to Integers - Quiz
Test your understanding of the lesson.
Practice Exercises
HTTP Status Code Analyzer
Create a program that uses an enumeration with explicit values to represent HTTP status codes. The program should demonstrate implicit conversion to integers and explicit conversion from integers.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!