C++ Enums
An enum (short for enumeration) is a user-defined type that represents a fixed set of named integer constants. Instead of scattering magic numbers like 0, 1, and 2 throughout your code to mean “Monday”, “Tuesday”, and “Wednesday”, an enum lets you write Monday, Tuesday, and Wednesday directly. This makes code self-documenting, easier to read, and much safer, because the compiler can catch mistakes like passing an invalid value where a specific set of options was expected.
C++ actually has two flavors of enum: the older unscoped enum (plain enum), inherited from C, and the modern scoped enum (enum class, introduced in C++11). Understanding both, and why the second one exists, is essential to writing safe modern C++.
Overview / How Enums Work
At the machine level, an enum is nothing more than an integer. Each enumerator (the named value, like Monday) is assigned an integer value at compile time. Unless you specify values explicitly, the compiler assigns 0 to the first enumerator, 1 to the second, and so on, incrementing by one for each subsequent name. This means an enum variable takes up the same amount of storage as its underlying type — by default this is usually int, but you can choose a smaller or larger integer type explicitly.
The key difference between the two kinds of enum is scope and implicit conversion:
- Unscoped enum (
enum Color { Red, Green, Blue };) — the enumerator names (Red,Green,Blue) are injected directly into the surrounding scope. They also implicitly convert toint, which is convenient but dangerous, since it allows nonsensical comparisons and arithmetic between unrelated enums. - Scoped enum (
enum class Color { Red, Green, Blue };) — the enumerator names must be qualified with the enum’s name, e.g.Color::Red. They do not implicitly convert tointor to any other enum type, which prevents whole categories of bugs. This is why modern C++ style guides recommendenum classalmost everywhere.
Internally, the compiler treats an enum value exactly like an integer of its underlying type. When you write a switch statement over an enum, the compiler generates the same kind of jump table or comparison chain it would for an integer switch. When you print an enum with std::cout, you are really printing its underlying integer, unless you write your own conversion function — enums have no built-in text representation.
Syntax
// Unscoped enum
enum Name { Enumerator1, Enumerator2, Enumerator3 };
// Unscoped enum with an explicit underlying type
enum Name : unsigned char { Enumerator1, Enumerator2 };
// Scoped enum (enum class) -- recommended in modern C++
enum class Name { Enumerator1, Enumerator2, Enumerator3 };
// Scoped enum with explicit underlying type and explicit values
enum class Name : int { Enumerator1 = 10, Enumerator2 = 20 };
| Part | Meaning |
|---|---|
enum / enum class |
Keyword that begins the declaration. enum class (or the equivalent enum struct) creates a scoped enum. |
Name |
The name of the new type. You declare variables of this type as Name variable;. |
: unsigned char |
Optional underlying type. Any integer type works (char, short, int, long, etc.). Defaults to int if omitted (unscoped enums may use a smaller type if all values fit, but this is implementation-defined; scoped enums default strictly to int). |
Enumerator1, Enumerator2, ... |
The named constants. Each gets an integer value, either automatic (previous value + 1, starting at 0) or explicitly assigned with = value. |
Examples
Example 1: A basic unscoped enum
#include <iostream>
using namespace std;
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
int main() {
Weekday today = WEDNESDAY;
cout << "Day number: " << today << endl;
if (today == WEDNESDAY) {
cout << "Midweek already!" << endl;
}
return 0;
}
Output:
Day number: 2
Midweek already!
Because MONDAY is the first enumerator, it gets value 0, so WEDNESDAY (the third enumerator) gets value 2. Notice that today prints as a plain number — std::cout has no idea an enum has a “name”, it just sees the underlying integer. Also notice that MONDAY, TUESDAY, etc. are visible directly in the surrounding scope, without any qualifier — this is the “unscoped” behavior that can cause name clashes in larger programs.
Example 2: A scoped enum (enum class) with an explicit underlying type
#include <iostream>
#include <string>
using namespace std;
enum class Color : unsigned char { Red, Green, Blue };
string colorName(Color c) {
switch (c) {
case Color::Red: return "Red";
case Color::Green: return "Green";
case Color::Blue: return "Blue";
}
return "Unknown";
}
int main() {
Color c = Color::Green;
cout << "Selected color: " << colorName(c) << endl;
cout << "Underlying value: " << static_cast<int>(c) << endl;
return 0;
}
Output:
Selected color: Green
Underlying value: 1
Here Color is declared with enum class, so its enumerators must always be written as Color::Red, Color::Green, Color::Blue — they never leak into the surrounding scope. Because scoped enums don’t implicitly convert to int, we must use static_cast<int>(c) to print the numeric value. The : unsigned char underlying type tells the compiler this enum only ever needs one byte of storage, which can matter when you have arrays of enums or care about memory layout.
Example 3: Using enum class in a small state machine
#include <iostream>
#include <string>
using namespace std;
enum class Light { Red, Yellow, Green };
Light& operator++(Light& l) {
switch (l) {
case Light::Red: l = Light::Green; break;
case Light::Green: l = Light::Yellow; break;
case Light::Yellow: l = Light::Red; break;
}
return l;
}
string lightName(Light l) {
switch (l) {
case Light::Red: return "Red";
case Light::Yellow: return "Yellow";
case Light::Green: return "Green";
}
return "?";
}
int main() {
Light current = Light::Red;
for (int i = 0; i < 4; ++i) {
cout << lightName(current) << endl;
++current;
}
return 0;
}
Output:
Red
Green
Yellow
Red
This example shows a realistic use of enum class: modeling a traffic light’s state machine. Because enum class gives you no arithmetic or increment operators for free, we write our own operator++ overload that defines exactly what “the next state” means. This is a common and idiomatic pattern — it keeps the safety of enum class while still letting you cycle through states cleanly with ++current.
How It Works Step by Step / Under the Hood
- At compile time, the compiler assigns each enumerator an integer value of the underlying type — automatically (0, 1, 2, …) unless you specify values explicitly.
- A variable of enum type is stored exactly like an integer of the underlying type — there is no runtime overhead, no hidden object, and no string data attached to it.
- For an unscoped enum, the enumerator names become ordinary identifiers in the enclosing scope (namespace, class, or block), and can silently convert to
intwherever an integer is expected. - For a scoped enum (
enum class), the enumerator names live inside the enum’s own scope, accessible only throughEnumName::Enumerator, and the compiler will not implicitly convert them to any other type — you must usestatic_cast. - A
switchstatement on an enum compiles down to the same code the compiler would generate for switching on an integer — comparisons or a jump table, depending on how many cases there are and how the compiler optimizes. - Converting an integer back into an enum (e.g.
static_cast<Color>(5)) is legal even if5doesn’t correspond to any enumerator — the language does not runtime-check this, so it’s your responsibility to keep values in range.
Common Mistakes
Mistake 1: Name collisions between unscoped enums
Because unscoped enumerators leak into the surrounding scope, two enums that share an enumerator name in the same scope will fail to compile:
enum Suit { Clubs, Diamonds, Hearts, Spades };
enum Rank { Ace, King, Queen, Jack, Clubs }; // error: 'Clubs' has already been declared
The fix is to use enum class, which scopes each enumerator to its own type, so identical names in different enums never collide:
enum class Suit { Clubs, Diamonds, Hearts, Spades };
enum class Rank { Ace, King, Queen, Jack, Clubs }; // fine: Suit::Clubs and Rank::Clubs are distinct
Mistake 2: Expecting enum class to implicitly convert to int
Unlike plain enums, a scoped enum will not silently become an integer, so code like this fails to compile:
enum class Status { Active, Inactive };
Status s = Status::Active;
int code = s; // error: cannot convert 'Status' to 'int' without a cast
The fix is to convert explicitly with static_cast, which makes the intent clear and is required by the language:
enum class Status { Active, Inactive };
Status s = Status::Active;
int code = static_cast<int>(s); // works
Best Practices
- Prefer
enum classover plainenumin new code — it prevents name clashes and accidental integer conversions. - Give enumerators clear, singular names (e.g.
Color::Red, notColor::COLOR_RED) since scoped enums already provide the namespace. - Specify an explicit underlying type (
: int,: unsigned char, etc.) when the exact size matters, such as when serializing data or storing many enum values in memory. - Use a
switchstatement without adefaultcase when you want the compiler to warn you if you forget to handle a newly added enumerator. - Write small helper functions (like
colorName()above) to convert enums to human-readable strings for logging and debugging — enums don’t do this automatically. - Avoid relying on the exact numeric values of enumerators unless you have explicitly assigned and documented them (for example, when the values must match a file format or network protocol).
Practice Exercises
- Define an
enum class Direction { North, East, South, West };and write a functionDirection turnRight(Direction d)that returns the direction 90 degrees clockwise fromd. Test it by starting atDirection::Northand calling it four times, printing the name each time. - Create an unscoped
enum HttpStatuswith explicit valuesOk = 200,NotFound = 404, andServerError = 500. Write a function that takes anintand prints the matching status name, or “Unknown” if it doesn’t match any enumerator. - Rewrite the
HttpStatusenum from the previous exercise as anenum classwith an underlying type ofint. Update your function to usestatic_castwhere needed, and explain in a comment why the scoped version is safer.
Summary
- An enum defines a type with a fixed, named set of integer values; it makes code more readable than using raw numbers.
- Unscoped enums (
enum) inject their enumerator names into the surrounding scope and implicitly convert toint, which can cause naming collisions and unsafe comparisons. - Scoped enums (
enum class) require qualified names likeColor::Redand never implicitly convert, making them the safer default choice in modern C++. - You can specify an explicit underlying type with
: typeto control the size and representation of an enum. - Enums have no built-in way to convert to text — write your own helper functions for logging and display.
- Converting an out-of-range integer into an enum with
static_castis legal but unchecked, so validate input carefully when it originates outside your program.
