Enumerations
An enumeration gives names to a fixed set of integer values. A day of the week, a state of a connection, a colour: the set is small, known at compile time, and the names are the documentation. C++ has two forms, and the newer one is the default choice.
The lesson moves from the old unscoped enum, whose names leak into the surrounding scope, to enum class, which keeps them inside and refuses implicit conversions. It ends with the two diagnostics that show why the scoped form is safer.
An unscoped enum
enum Color { red, green, blue }; declares three constants with the values 0, 1 and 2. The names are visible in the surrounding scope, so green can be written without a qualifier, and they convert to int without a cast.
That conversion is convenient in old code and dangerous in new code: a Color can be passed to a function that takes an int, added to another enum, and printed as a number. The first line of output prints the value of green, which is 1, not the word.
Unscoped enums are still the right tool for a set of flags whose values are bit patterns, because the implicit conversion to int is what the bitwise operators need. For everything else, the next section shows the scoped form.
#include <iostream>
enum Color { red, green, blue };
int main()
{
Color c = green;
std::cout << c << ' ' << (c == green) << '\n';
std::cout << red << ' ' << green << ' ' << blue << '\n';
}
g++ -std=c++23 -Wall -Wextra unscoped.cpp -o unscoped
./unscoped
1 1
0 1 2
enum class
enum class Color { red, green, blue }; keeps the names inside Color, so the value is written Color::blue and there is no bare blue in the scope. The type does not convert to int implicitly, which means it cannot be mixed with numbers by accident.
Printing one takes a cast, and the cast is the place where the program states that it wants the number. The program casts once and compares once, and the comparison needs no cast because both sides have the same type.
This is the form to use for every new enumeration. The extra qualification at the call site is the feature: a function taking Color cannot be called with the int 2, and a Color cannot be stored in an int without saying so.
#include <iostream>
enum class Color { red, green, blue };
int main()
{
Color c = Color::blue;
std::cout << static_cast<int>(c) << '\n';
std::cout << (c == Color::blue) << '\n';
}
g++ -std=c++23 -Wall -Wextra scoped.cpp -o scoped
./scoped
2
1
The underlying type
Every enumeration has an underlying integer type, int by default. enum class Status : std::uint8_t chooses an unsigned char, which makes the enum one byte instead of four and fixes the range to 0 to 255.
The size matters when the value is stored in a struct that is written to a file or sent over a wire, and when an array of them would otherwise waste three bytes per element. The program prints sizeof for the enum and for an int.
The fixed-width types come from <cstdint>; lesson 007 introduced the integer types and their sizes.
#include <cstdint>
#include <iostream>
enum class Status : std::uint8_t { ok, warning, error };
int main()
{
std::cout << sizeof(Status) << ' ' << sizeof(int) << '\n';
std::cout << static_cast<int>(Status::error) << '\n';
}
g++ -std=c++23 -Wall -Wextra underlying.cpp -o underlying
./underlying
1 4
2
A switch that misses a case
A switch over an enum is the natural way to turn a value into text or behaviour. When a case is missing and there is no default, GCC warns that an enumeration value is not handled, which turns a forgotten case into a compile-time reminder.
The function below handles red and green and forgets blue. The warning names the missing value, and the program still runs: blue falls through to the return after the switch and prints unknown.
The warning appears with -Wall, and it is the reason a switch over an enum should not have a default: with a default, a new value added to the enum compiles silently and takes the default path.
#include <iostream>
enum class Color { red, green, blue };
const char* name(Color c)
{
switch (c) {
case Color::red:
return "red";
case Color::green:
return "green";
}
return "unknown";
}
int main()
{
std::cout << name(Color::red) << ' ' << name(Color::blue) << '\n';
}
g++ -std=c++23 -Wall -Wextra switch.cpp -o switch_enum
./switch_enum
switch.cpp: In function 'const char* name(Color)':
switch.cpp:7:12: warning: enumeration value 'blue' not handled in switch [-Wswitch]
7 | switch (c) {
| ^
red unknown
A scoped enum does not convert
The compiler refuses to put a Color into an int, because the two types are different and the conversion would silently attach a number to a value that is only meaningful as a name.
The message says the conversion cannot be done in initialization and prints the value and its type. The fix is static_cast<int>(Color::red), which is the explicit form, or a function that maps the enum to text.
The same rule blocks comparisons between different enum types and arithmetic on an enum class. Both are mistakes the scoped form is designed to catch, and both are ordinary code in a program that used unscoped enums before C++11.
enum class Color { red, green, blue };
int main()
{
int value = Color::red;
return value;
}
g++ -std=c++23 -Wall -Wextra no_conversion.cpp -o no_conversion
no_conversion.cpp: In function 'int main()':
no_conversion.cpp:5:24: error: cannot convert 'Color' to 'int' in initialization
5 | int value = Color::red;
| ~~~~~~~^~~
| |
| Color
Exercises
- Add a function to_text(Color) that handles all three values and check that the -Wswitch warning disappears.
- Declare an enum class with an underlying type of std::uint16_t and print sizeof for it and for the default enum.
- Change the scoped program to store Color::blue in an int with static_cast and print it. Then try without the cast and read the error.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is the problem with an unscoped enum?
Answer
Its names leak into the surrounding scope and it converts to int
The enumerators are ordinary names in the enclosing scope, and the value converts implicitly.
-
What does
enum classchange?Answer
The names are scoped and there is no implicit conversion to int
The qualified name and the explicit cast are the two visible differences.
-
What does the underlying type of an enum decide?
Answer
The size and range of the stored values
enum class small : unsigned charstores one byte, which the program shows with sizeof. -
What does a switch over an enum warn about when a value is missing?
Answer
enumeration value not handled in switch
-Wswitch names each enumerator that has no case, which is how a new value is noticed.
-
What does this program print?
#include <iostream> enum color { red, green, blue }; int main() { std::cout << green << ' ' << red + 1 << " / "; for (int value = red; value <= blue; ++value) std::cout << value << ' '; }Answer
1 1 / 0 1 2
An unscoped enumerator converts to int, and the loop walks the three values from zero.
-
What does this program print?
#include <iostream> enum class color { red = 2, blue = 1 }; int main() { std::cout << static_cast<int>(color::red) << ' ' << (color::blue < color::red); }Answer
2 1
The scoped enum needs the cast for printing, and the comparison of the two values is true.
-
What does this program print?
#include <iostream> enum class small : unsigned char { one = 1, two = 2 }; int main() { std::cout << static_cast<int>(small::one) << ' ' << sizeof(small) << " / " << static_cast<int>(small::two); }Answer
1 1 / 2
The underlying type is unsigned char, so the enum is one byte, and the values are 1 and 2.
-
What does this program print for red and for blue?
#include <iostream> enum class color { red, green, blue }; const char* name(color value) { switch (value) { case color::red: return "red"; case color::green: return "green"; default: return "unknown"; } } int main() { std::cout << name(color::red) << ' ' << name(color::blue); }Answer
red unknown
The red case returns its name, and blue reaches the default because it has no case.
-
What does
int value = Color::red;with a scoped enum produce?Answer
cannot convert 'Color' to 'int' in initialization
A scoped enum does not convert implicitly; the cast
static_cast<int>(Color::red)is required. -
Why is
enum classthe default choice in new code?Answer
It keeps the names and the conversions under control
The scoped form prevents accidental integer conversions and name collisions.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev