C++ Enumerations: enum vs enum class Complete Guide
What Are Enumerations?
An enumeration defines a type that can hold one of a fixed set of named values. Instead of writing int status = 2; and hoping you remember that 2 means “error,” you write Status status = Status::Error;. The name says exactly what the value represents.
Enums are everywhere in real codebases: HTTP status codes, file open modes, game states, log levels, UI themes, configuration options. They make code self-documenting and let the compiler catch bugs that raw integers would hide. C++ has two flavors — old-style enum and modern enum class. Modern C++ strongly prefers enum class, and this lesson explains why.
Old-Style enum
The traditional C-style enum defines named constants that are implicitly integers:
#include <iostream>
using namespace std;
enum Color {
Red, // 0
Green, // 1
Blue // 2
};
enum Size {
Small = 10,
Medium = 20,
Large = 30
};
int main() {
Color c = Red;
Size s = Large;
cout << "Red = " << Red << endl; // 0
cout << "Large = " << Large << endl; // 30
// Implicit conversion to int
int value = c; // OK, value = 0
cout << value << endl;
return 0;
}
By default, enumerators start at 0 and increment by 1. You can assign explicit values to any or all of them. If you set Medium = 20, the next unassigned enumerator would be 21 (not that we need it here).
Problems with Old Enums
Old-style enums have two serious flaws that make them dangerous in large codebases:
Problem 1: Name leaking. Enumerator names are injected into the enclosing scope:
enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green }; // ERROR: Red and Green already defined!
Both enums try to define Red and Green in the same scope. The compiler rejects this. In large projects with hundreds of files, name collisions like this are a real problem.
Problem 2: Implicit conversion to int. Old enums convert to integers silently, enabling nonsensical comparisons:
enum Color { Red, Green, Blue };
enum Fruit { Apple, Banana, Cherry };
Color c = Red;
Fruit f = Apple;
if (c == f) { // Compiles! Compares 0 == 0 → true
// This is logically meaningless — comparing a color to a fruit
}
The compiler does not warn about this. You are comparing completely unrelated types, and it silently says “equal” because both happen to be 0.
enum class (Scoped Enumerations)
C++11 introduced enum class to fix both problems. Enumerator names are scoped to the enum, and there is no implicit conversion to integers:
#include <iostream>
using namespace std;
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green }; // OK! No conflict
int main() {
Color c = Color::Red; // must qualify with Color::
TrafficLight t = TrafficLight::Red; // different Red, no collision
// cout << c; // ERROR: no implicit conversion to int
// if (c == t) {} // ERROR: cannot compare Color to TrafficLight
// int x = c; // ERROR: no implicit conversion
if (c == Color::Red) {
cout << "It is red" << endl;
}
return 0;
}
Every problem solved. Names don’t leak (Color::Red and TrafficLight::Red coexist). You cannot accidentally compare a Color to a TrafficLight. You cannot silently convert to int. The compiler enforces type safety.
Rule of thumb: always use enum class in new C++ code. There is no good reason to use old-style enum unless you are interfacing with C code or legacy APIs.
Specifying the Underlying Type
By default, the compiler chooses an integer type large enough to hold all enumerator values (usually int). You can specify the underlying type explicitly:
#include <iostream>
#include <cstdint>
using namespace std;
// Use uint8_t to save memory (only need 0-2)
enum class Direction : uint8_t {
North, South, East, West
};
// Use int64_t for large values
enum class Permission : int64_t {
None = 0,
Read = 1LL << 32,
Write = 1LL << 33,
Execute = 1LL << 34
};
int main() {
cout << "sizeof(Direction) = " << sizeof(Direction) << endl; // 1
cout << "sizeof(Permission) = " << sizeof(Permission) << endl; // 8
return 0;
}
This is useful for memory-sensitive applications (embedded systems, network protocols) where you want precise control over the size of enum values.
Enums with switch Statements
Enums and switch are natural partners. The compiler can warn you if you forget to handle a case (with -Wswitch), making your code safer:
#include <iostream>
#include <string>
using namespace std;
enum class LogLevel { Debug, Info, Warning, Error, Fatal };
string levelToString(LogLevel level) {
switch (level) {
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warning: return "WARNING";
case LogLevel::Error: return "ERROR";
case LogLevel::Fatal: return "FATAL";
}
return "UNKNOWN"; // unreachable if all cases handled
}
void log(LogLevel level, const string& message) {
cout << "[" << levelToString(level) << "] " << message << endl;
}
int main() {
log(LogLevel::Info, "Application started");
log(LogLevel::Warning, "Disk space low");
log(LogLevel::Error, "Connection failed");
return 0;
}
If you later add LogLevel::Trace but forget to update the switch, compiling with -Wswitch (enabled by -Wall) will warn you. This is one of the biggest practical benefits of enums — the compiler becomes your safety net.
Converting Between Enums and Integers
Sometimes you need to convert between enums and integers (for serialization, array indexing, or interfacing with C APIs). With enum class, you must use explicit casts:
#include <iostream>
using namespace std;
enum class Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday };
int main() {
// Enum to int: static_cast
Weekday day = Weekday::Wednesday;
int index = static_cast<int>(day); // 2
cout << "Wednesday = " << index << endl;
// Int to enum: static_cast
Weekday fromInt = static_cast<Weekday>(4); // Friday
if (fromInt == Weekday::Friday) {
cout << "It's Friday!" << endl;
}
// Use enum as array index
const char* names[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
cout << names[static_cast<int>(day)] << endl; // Wed
return 0;
}
The explicit static_cast is intentionally verbose — it makes every conversion visible in code review. If you find yourself casting enums frequently, consider whether an old-style enum (with its implicit conversions) is more appropriate, or whether your design needs rethinking.
Bit Flags with Enums
A common pattern is using enums as bit flags, where each value represents a power of 2 and values can be combined with bitwise OR:
#include <iostream>
using namespace std;
enum class FilePermission : unsigned {
None = 0,
Read = 1 << 0, // 1
Write = 1 << 1, // 2
Execute = 1 << 2 // 4
};
// Overload bitwise operators for the enum
FilePermission operator|(FilePermission a, FilePermission b) {
return static_cast<FilePermission>(
static_cast<unsigned>(a) | static_cast<unsigned>(b));
}
FilePermission operator&(FilePermission a, FilePermission b) {
return static_cast<FilePermission>(
static_cast<unsigned>(a) & static_cast<unsigned>(b));
}
bool hasPermission(FilePermission perms, FilePermission check) {
return (perms & check) == check;
}
int main() {
FilePermission myPerms = FilePermission::Read | FilePermission::Write;
cout << boolalpha;
cout << "Has Read? " << hasPermission(myPerms, FilePermission::Read) << endl; // true
cout << "Has Execute? " << hasPermission(myPerms, FilePermission::Execute) << endl; // false
// Add execute permission
myPerms = myPerms | FilePermission::Execute;
cout << "Has Execute? " << hasPermission(myPerms, FilePermission::Execute) << endl; // true
return 0;
}
The operator overloads let you use | and & naturally with the enum type. This is type-safe — you cannot accidentally OR a FilePermission with a LogLevel. This pattern appears in real APIs like OpenGL flags, Windows API constants, and Unix file permissions.
Iterating Over Enums
C++ does not have built-in enum iteration. Here are two practical approaches:
#include <iostream>
#include <array>
using namespace std;
enum class Season { Spring, Summer, Autumn, Winter, COUNT };
// Approach 1: Use COUNT as a sentinel
void printAllSeasons() {
const char* names[] = {"Spring", "Summer", "Autumn", "Winter"};
for (int i = 0; i < static_cast<int>(Season::COUNT); i++) {
cout << names[i] << " ";
}
cout << endl;
}
// Approach 2: Use a constexpr array of all values
constexpr array<Season, 4> ALL_SEASONS = {
Season::Spring, Season::Summer, Season::Autumn, Season::Winter
};
int main() {
printAllSeasons(); // Spring Summer Autumn Winter
// Approach 2: range-based for
const char* names[] = {"Spring", "Summer", "Autumn", "Winter"};
for (Season s : ALL_SEASONS) {
cout << names[static_cast<int>(s)] << " ";
}
cout << endl;
return 0;
}
The COUNT sentinel approach is simple but fragile — if enumerators have non-sequential values, it breaks. The explicit array approach is safer and more flexible. C++26 will likely add reflection features that enable automatic enum iteration.
Printing Enums
Since enum class has no implicit conversion to int, you cannot just cout << myEnum. The standard approach is to overload operator<<:
#include <iostream>
using namespace std;
enum class HttpStatus {
OK = 200,
NotFound = 404,
InternalError = 500,
BadGateway = 502
};
ostream& operator<<(ostream& os, HttpStatus status) {
switch (status) {
case HttpStatus::OK: return os << "200 OK";
case HttpStatus::NotFound: return os << "404 Not Found";
case HttpStatus::InternalError: return os << "500 Internal Server Error";
case HttpStatus::BadGateway: return os << "502 Bad Gateway";
}
return os << "Unknown (" << static_cast<int>(status) << ")";
}
int main() {
HttpStatus code = HttpStatus::NotFound;
cout << "Response: " << code << endl;
// Output: Response: 404 Not Found
return 0;
}
This pattern appears in every professional C++ codebase. Some teams use macros or code generation to auto-create these toString functions, since maintaining them by hand is tedious for large enums.
Real-World Example: State Machine
Enums are the backbone of state machines — a pattern used in game AI, network protocols, parsers, and UI logic:
#include <iostream>
#include <string>
using namespace std;
enum class ConnectionState {
Disconnected,
Connecting,
Connected,
Disconnecting
};
ostream& operator<<(ostream& os, ConnectionState s) {
switch (s) {
case ConnectionState::Disconnected: return os << "Disconnected";
case ConnectionState::Connecting: return os << "Connecting";
case ConnectionState::Connected: return os << "Connected";
case ConnectionState::Disconnecting: return os << "Disconnecting";
}
return os << "Unknown";
}
class Connection {
ConnectionState state = ConnectionState::Disconnected;
string host;
public:
Connection(const string& h) : host(h) {}
void connect() {
if (state != ConnectionState::Disconnected) {
cout << "Cannot connect: already " << state << endl;
return;
}
state = ConnectionState::Connecting;
cout << "Connecting to " << host << "..." << endl;
// Simulate successful connection
state = ConnectionState::Connected;
cout << "Connected to " << host << endl;
}
void disconnect() {
if (state != ConnectionState::Connected) {
cout << "Cannot disconnect: currently " << state << endl;
return;
}
state = ConnectionState::Disconnecting;
cout << "Disconnecting from " << host << "..." << endl;
state = ConnectionState::Disconnected;
cout << "Disconnected" << endl;
}
void sendData(const string& data) {
if (state != ConnectionState::Connected) {
cout << "Cannot send: not connected (state: " << state << ")" << endl;
return;
}
cout << "Sending: " << data << endl;
}
ConnectionState getState() const { return state; }
};
int main() {
Connection conn("sudoflare.com");
conn.sendData("hello"); // Cannot send: not connected
conn.connect(); // Connecting... Connected
conn.sendData("hello"); // Sending: hello
conn.connect(); // Cannot connect: already Connected
conn.disconnect(); // Disconnecting... Disconnected
conn.disconnect(); // Cannot disconnect: currently Disconnected
return 0;
}
The enum enforces that only valid state transitions happen. If you try to send data while disconnected, the code catches it immediately. Without enums, you would track state with integers or strings — both of which are error-prone and hard to debug.
Practice Exercises
Exercise 1: Create an enum class CardSuit and enum class CardRank for a playing card. Write a Card struct that holds both and overload operator<< to print cards like “Ace of Spades.”
Exercise 2: Implement a TextStyle enum with bit flags for Bold, Italic, Underline, and Strikethrough. Overload |, &, and << so you can write TextStyle style = TextStyle::Bold | TextStyle::Italic;.
Exercise 3: Build a simple traffic light state machine with states Red, Yellow, Green. Implement a next() function that cycles through states and prints the current light. Add a timer concept (each state lasts a different number of ticks).
Exercise 4: Create a Month enum class with all 12 months. Write functions to get the number of days in a month (accounting for leap years), the month name as a string, and the next/previous month with wraparound.
Summary
Enumerations give names to integer constants and make your intent explicit. Always prefer enum class over old-style enum — it prevents name collisions and type confusion. You learned how to specify underlying types for memory control, use enums with switch statements to get compiler warnings for unhandled cases, create bit flags for combinable options, and build state machines that enforce valid transitions. In the next lesson, you will learn about copy constructors and the Rule of 3/5/0 — essential concepts for writing correct C++ classes that manage resources.