Enums & enum class
Many values in a program come from a small, fixed set of choices: the coach class on a train ticket, the status of an order, the day of the week, the level of a log message. You could store them as numbers (2 for AC 2-tier) or text ("2A"), but numbers are hard to read and text is easy to misspell. An enumeration (enum) lets you create your own type whose values have names.
In this lesson you will meet the old C-style enum, learn why modern C++ prefers enum class, and use enums with switch, arrays and C++20's using enum.
The problem with plain enums
C++ inherited a simple enum from C. Each name becomes a constant integer, starting from 0.
#include <print>
enum Colour { red, green, blue }; // red = 0, green = 1, blue = 2
// enum Signal { red, amber, green }; // error: red and green already exist
int main() {
Colour wall = green;
int number = wall; // silently converts to int
std::println("wall = {}", number);
if (wall == 1) { // comparing with a plain number compiles
std::println("Green, but the code does not say so");
}
}wall = 1 Green, but the code does not say so
Two problems show up here. First, the names red, green and blue leak into the surrounding scope, so a second enum with a value called red cannot exist in the same place. Second, the values convert to int without warning, so the compiler accepts meaningless code like wall == 1 or wall + 7. These unscoped enums are still everywhere in C libraries and older C++ code.
enum class: scoped and strongly typed
C++11 added the scoped enumeration, written enum class. Its names live inside the enum, so you write Coach::ac3, and it never converts to a number by accident.
#include <print>
enum class Coach { sleeper, ac3, ac2, ac1 };
enum class Signal { red, amber, green };
enum class Colour { red, green, blue }; // fine: no clash with Signal::red
int fare_per_km(Coach c) { // made-up rates in rupees
switch (c) {
case Coach::sleeper: return 1;
case Coach::ac3: return 2;
case Coach::ac2: return 3;
case Coach::ac1: return 5;
}
return 0; // never reached for valid values
}
int main() {
Coach booked = Coach::ac3;
std::println("Fare for 500 km: Rs. {}", 500 * fare_per_km(booked));
// int n = booked; // error: no implicit conversion
// if (booked == 1) {} // error: cannot compare with int
}Fare for 500 km: Rs. 1000
Because fare_per_km switches over every value of Coach, compiling with -Wall gives a warning if you later add a new coach class and forget to handle it here. That warning alone has saved many real programs.
Underlying types and converting to numbers
Every enum stores its values in an integer type called the underlying type. For enum class it is int unless you choose another one after a colon. You can also give values explicitly. When you really need the number, for example to save it in a file, convert on purpose:
#include <cstdint>
#include <print>
#include <utility>
enum class Priority : std::uint8_t { low = 1, medium = 5, high = 10 };
int main() {
Priority p = Priority::high;
std::println("Stored as: {}", std::to_underlying(p)); // C++23
std::println("Also: {}", static_cast<int>(p));
std::println("Size: {} byte", sizeof(Priority));
auto from_file = static_cast<Priority>(5);
std::println("Is medium? {}", from_file == Priority::medium);
}Stored as: 10 Also: 10 Size: 1 byte Is medium? true
std::to_underlying (header <utility>, C++23) converts an enum to its underlying type without you having to name that type. Before C++23 you wrote static_cast<int>(p). Converting the other way, from a number to an enum, also needs static_cast. Be careful: the compiler does not check that the number matches a named value, so validate numbers that come from users or files first. Choosing std::uint8_t as the underlying type makes each value take one byte, useful when you store millions of them.
Enums with switch and using enum
Enums do not know their own names, so std::println("{}", Coach::ac3) does not compile. The usual solution is a small function that turns each value into text. C++20's using enum lets you drop the Coach:: prefix inside such a function:
#include <array>
#include <print>
#include <string_view>
enum class Coach { sleeper, ac3, ac2, ac1 };
std::string_view to_text(Coach c) {
using enum Coach; // C++20: names usable without Coach::
switch (c) {
case sleeper: return "Sleeper";
case ac3: return "AC 3-tier";
case ac2: return "AC 2-tier";
case ac1: return "AC First Class";
}
return "Unknown";
}
int main() {
constexpr std::array all_coaches = {Coach::sleeper, Coach::ac3, Coach::ac2, Coach::ac1};
for (Coach c : all_coaches) {
std::println("{:<15} code {}", to_text(c), static_cast<int>(c));
}
}Sleeper code 0 AC 3-tier code 1 AC 2-tier code 2 AC First Class code 3
C++ has no built-in way to loop over all values of an enum, so the program keeps them in a constexpr std::array. The upcoming C++26 adds reflection, which can list an enum's values and names automatically; see C++26 Preview.
Common mistakes
- Expecting
enum classto print as text. It has no formatter; write ato_textfunction or convert to a number. - Using
Coachvalues without the prefix. Outsideusing enum, you must writeCoach::ac2, notac2. - Trusting
static_castfrom numbers.static_cast<Coach>(42)compiles but is not a named coach. Check the range first. - Adding a
default:to every enum switch. Adefaulthides the helpful "enumeration value not handled" warning when you add new values. Handle every value explicitly instead.
Key points
- An enum gives names to a fixed set of values.
- Old unscoped
enumnames leak into the surrounding scope and convert tointsilently. enum classvalues are scoped (Coach::ac3) and never convert implicitly.- Choose the underlying type with
enum class Name : std::uint8_t; convert withstd::to_underlying(C++23) orstatic_cast. using enum(C++20) removes the prefix inside a block; aswitchcovering every value lets the compiler warn when one is missing.
Exercise
Create enum class OrderStatus { placed, packed, shipped, delivered, cancelled }; for an online shop in Kochi. Write std::string_view to_text(OrderStatus) using using enum, and a function bool can_cancel(OrderStatus) that returns true only for placed and packed. Loop over all five statuses and print each name and whether it can be cancelled.
Show solution
#include <array>
#include <print>
#include <string_view>
enum class OrderStatus { placed, packed, shipped, delivered, cancelled };
std::string_view to_text(OrderStatus s) {
using enum OrderStatus;
switch (s) {
case placed: return "Placed";
case packed: return "Packed";
case shipped: return "Shipped";
case delivered: return "Delivered";
case cancelled: return "Cancelled";
}
return "Unknown";
}
bool can_cancel(OrderStatus s) {
return s == OrderStatus::placed || s == OrderStatus::packed;
}
int main() {
using enum OrderStatus;
constexpr std::array all = {placed, packed, shipped, delivered, cancelled};
for (auto s : all) {
std::println("{:<10} can cancel: {}", to_text(s), can_cancel(s));
}
}Placed can cancel: true Packed can cancel: true Shipped can cancel: false Delivered can cancel: false Cancelled can cancel: false
using enum works inside main too, which keeps the array short.
Quick Quiz
1. What is the main advantage of enum class over a plain enum?
Scoped enums keep their names inside the enum and block accidental mixing with numbers.
2. How do you get the number stored in Priority::high in C++23 without naming the underlying type?
std::to_underlying from <utility> returns the value as the enum's underlying type.
3. What does using enum Coach; do?
using enum (C++20) brings the enumerator names into the current scope.
4. Why can a default: label in an enum switch be harmful?
Without default, -Wall warns about unhandled enumerators, which catches forgotten cases.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
