You might be surprised to find out that enumerators are actually just named integral constants (and as a result, enumerated types just hold an integral value).
This is similar to the case with chars (%Failed lesson reference, id XX%). Consider:
1 |
char ch { 'A' }; |
A char is really just a small integer, and the character 'A'
gets converted to an integral value (in this case, 65
) and stored.
When we define an enumerator, each enumerator is automatically assigned an integer value based on its position in the enumerator list. By default, the first enumerator is assigned the integer value 0, and each subsequent enumerator has a value one greater than the previous enumerator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> enum Color { black, // assigned 0 red, // assigned 1 blue, // assigned 2 green, // assigned 3 white, // assigned 4 cyan, // assigned 5 yellow, // assigned 6 magenta // assigned 7 }; int main() { Color paint{ red }; // under the hood, this stores the integral value 1 return 0; } |
It is possible to explicitly define the value of enumerator. These integer 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.
1 2 3 4 5 6 7 8 9 |
enum Animal { cat = -3, dog, // assigned -2 pig, // assigned -1 horse = 5, giraffe = 5, // shares same value as horse chicken // assigned 6 }; |
Note in this case, horse
and giraffe
have been given the same value. When this happens, the enumerators become non-distinct -- essentially, horse
and giraffe
are interchangeable. Although C++ allows it, assigning the same value to two enumerators in the same enumeration should generally be avoided.
Best practice
Avoid assigning explicit values to your enumerators unless you have a compelling reason to do so.
Unscoped enumeration evaluation and output
Consider the following program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> enum Color { black, // assigned 0 red, // assigned 1 blue, // assigned 2 green, // assigned 3 white, // assigned 4 cyan, // assigned 5 yellow, // assigned 6 magenta // assigned 7 }; int main() { Color paint{ red }; std::cout << paint; // what does this do? return 0; } |
This prints:
1
When an enumerated value 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 << paint
, the compiler will first look to see if std::cout
knows how to print an object of type Color (because paint is of type Color). It doesn't.
If the compiler can't find a match, the compiler will then implicitly convert an unscoped enumerator to an integer value. Because std::cout
does know how to print an integral value, the value in paint
gets converted to an integer and printed as integer value 1
.
Switches and enumerations
DISCUSS HERE and update examples
Printing enumerator names
As you saw in the example above, trying to print an enumerated value using std::cout
results in the integer value of the enumerator being printed.
If you want to print an enumerator as text, you'll need to write a function to do that yourself. The most primitive way to do this is to write a print function that uses a series of if-statements
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> enum Color { black, red, blue, }; // This function could be improved with a switch statement instead of chain of if-statements void printColor(Color color) { if (color == black) std::cout << "black"; else if (color == red) std::cout << "red"; else if (color == blue) std::cout << "blue"; } int main() { Color shirt { blue }; std::cout << "Your shirt is color: "; printColor(shirt); std::cout << '\n'; return 0; } |
This prints:
blue
For advanced readers
Once you've learned to use switch statements (%Failed lesson reference, no id%), you'll probably want to use those instead of a bunch of if/else statements, as it's likely to be more performant and more readable.
The above method has one major downside: because printColor
returns void, printColor
can't be called on the same line that std::cout is used. As a result, printing a single line of text gets broken into 3 statements!
There are two good solutions for fixing this, both of which use concepts we haven't talked about yet. First, instead of having your function print the enumerator, you can have it return the enumerator name as a std::string_view or std::string:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> #include <string_view> // for C++17 or higher // #include <string> // for C++14 or lower enum Color { black, red, blue, }; // Return the color name as a string // This function could be improved with a switch statement instead of chain of if-statements std::string_view getColorName(Color color) // use std::string instead for C++14 or lower { if (color == black) return "black"; else if (color == red) return "red"; else if (color == blue) return "blue"; else return "???"; } int main() { Color shirt { blue }; std::cout << "Your shirt is color: " << getColorName(shirt) << '\n'; return 0; } |
You don't need to know how std::string_view
or std::string
work here (we'll explain them later in this chapter) -- just know they help return the string literal back to the caller, and that std::cout
knows how to print such text values.
An alternative method is to teach std::cout
how to print the value of your program-defined enumerated type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <iostream> enum Color { black, red, blue, }; // Teach std::cout how to print a Color // Consider this magic for now since we haven't explained any of the concepts it uses yet // This function could be improved with a switch statement instead of chain of if-statements std::ostream& operator<<(std::ostream& out, Color c) { if (c == black) return out << "black"; if (c == red) return out << "red"; if (c == blue) return out << "blue"; return out; } int main() { Color shirt{ black }; std::cout << "Your shirt is color: " << shirt; // it works! return 0; } |
Unscoped enumerator input
While the compiler will implicitly convert unscoped enumerators to an integer, it will not implicitly convert an integer to an enumerated value. The following will produce a compiler error:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> enum Pet { cat, // assigned 0 dog, // assigned 1 pig, // assigned 2 whale, // assigned 3 }; int main() { Pet pet { 2 }; // compile error: integer value 1 won't implicitly convert to a Pet pet = 1; // compile error: integer value 2 won't implicitly convert to a Pet return 0; } |
However, you can force the compiler to convert an integer to an unscoped enumerator using static_cast
:
1 |
pet = static_cast<Pet>(2); // convert integer 2 to a Pet (in this case, enumerator pig) |
There's one particular case where this can be useful. The compiler won't let you input an enumerated value using std::cin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> enum Pet { cat, // assigned 0 dog, // assigned 1 pig, // assigned 2 whale, // assigned 3 }; int main() { Pet pet { pig }; std::cin >> pet; // compile error, std::cin doesn't know how to input a Pet return 0; } |
To work around this, we can read in an integer, and use static_cast
to convert the integer to an enumerator of the appropriate enumerated type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> enum Pet { cat, // assigned 0 dog, // assigned 1 pig, // assigned 2 whale, // assigned 3 }; int main() { std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): "; int input{}; std::cin >> input; // input an integer Pet pet{ static_cast<Pet>(input) }; // static_cast our integer to a Pet return 0; } |
Enumerated types are considered part of the integer family of types, and it's up to the compiler to determine how much memory to allocate for an enum variable. The C++ standard says the enum size needs to be large enough to represent all of the enumerator values. Most often, it will make enum variables the same size as a standard int.
However, it is possible to specify a different underlying type. For example, if you are working in some bandwidth-sensitive context (e.g. sending data over a network) you may want to specify an smaller type:
1 2 3 4 5 6 7 |
// Use an 8-bit unsigned integer as the enum base enum Color : std::uint_least8_t { black, red, blue, }; |
Since enumerators aren't usually used for arithmetic or comparisons, it's generally safe to use an unsigned integer if desired.
Best practice
Specify the base type of an enumeration only when necessary.
Quiz time
Question #1
True or false. Enumerators can be:
* Given an integer value
* Not assigned a value
* Given a floating point value
* Given a negative value
* Given a non-unique value
* Initialized with the value of prior enumerators (e.g. COLOR_MAGENTA = COLOR_RED)
Leave a Reply