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 to int, 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 to int or to any other enum type, which prevents whole categories of bugs. This is why modern C++ style guides recommend enum class almost 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 int wherever an integer is expected.
  • For a scoped enum (enum class), the enumerator names live inside the enum’s own scope, accessible only through EnumName::Enumerator, and the compiler will not implicitly convert them to any other type — you must use static_cast.
  • A switch statement 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 if 5 doesn’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 class over plain enum in new code — it prevents name clashes and accidental integer conversions.
  • Give enumerators clear, singular names (e.g. Color::Red, not Color::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 switch statement without a default case 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 function Direction turnRight(Direction d) that returns the direction 90 degrees clockwise from d. Test it by starting at Direction::North and calling it four times, printing the name each time.
  • Create an unscoped enum HttpStatus with explicit values Ok = 200, NotFound = 404, and ServerError = 500. Write a function that takes an int and prints the matching status name, or “Unknown” if it doesn’t match any enumerator.
  • Rewrite the HttpStatus enum from the previous exercise as an enum class with an underlying type of int. Update your function to use static_cast where 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 to int, which can cause naming collisions and unsafe comparisons.
  • Scoped enums (enum class) require qualified names like Color::Red and never implicitly convert, making them the safer default choice in modern C++.
  • You can specify an explicit underlying type with : type to 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_cast is legal but unchecked, so validate input carefully when it originates outside your program.