Enum in C++: Unscoped vs Enum class with Examples

An Enum in C++ represents a set of named integral constants. Enums enhance readability (no more magic numbers) and make switches and state machines safer and more expressive.

1. Unscoped enum

The classic C++ enum is unscoped: its names are introduced into the surrounding scope and implicitly convert to int. This is convenient but can lead to name collisions and accidental integer use.

#include <iostream>
enum Color { Red, Green, Blue }; // unscoped
int main() {
    Color c = Red;  // implicit to int allowed
    if (c == Red) std::cout << "It's red\n";
}

Output

It's red

📝 Try it Yourself: Add a switch on Color and print a message for each enumerator.

1.1 Unscoped enum: Explicit start value and auto-increment

In an unscoped enum, if the first enumerator is assigned a specific integer value, subsequent unassigned enumerators automatically increment from that value by 1.

#include <iostream>
enum Rank {
    Rookie = 1, // start at 1 (not 0)
    Soldier,    // 2
    Elite,      // 3
    Legend = 10,
    Mythic      // 11
};
int main() {
    std::cout << "Rookie=" << static_cast<int>(Rookie) << "\n"
              << "Soldier=" << static_cast<int>(Soldier) << "\n"
              << "Elite=" << static_cast<int>(Elite) << "\n"
              << "Legend=" << static_cast<int>(Legend) << "\n"
              << "Mythic=" << static_cast<int>(Mythic) << "\n";
}

Output

Rookie=1
Soldier=2
Elite=3
Legend=10
Mythic=11

📝 Try it Yourself: Change Rookie = 0 (or -5) and verify that Soldier and Elite auto-increment accordingly. Then set Legend = 100 and confirm Mythic becomes 101.

1.2 Unscoped enum: Character literals as enumerator values

Enumerator values are integral constants, so using character literals is valid. By default the underlying type of an unscoped enum is int, and character literals like 'A' are also integral constants. Unassigned enumerators continue from the previous code point.

#include <iostream>
enum Token {
    TokA = 'A', // 65 in ASCII/UTF-8
    TokB,       // 66 ('B')
    TokZ = 'Z', // 90
    TokNext     // 91 ('[')
};
int main() {
    std::cout << "TokA char=" << static_cast<char>(TokA)
              << " int=" << static_cast<int>(TokA) << "\n";
    std::cout << "TokB char=" << static_cast<char>(TokB)
              << " int=" << static_cast<int>(TokB) << "\n";
    std::cout << "TokZ char=" << static_cast<char>(TokZ)
              << " int=" << static_cast<int>(TokZ) << "\n";
    std::cout << "TokNext char=" << static_cast<char>(TokNext)
              << " int=" << static_cast<int>(TokNext) << "\n";
}

Output (example)

TokA char=A int=65
TokB char=B int=66
TokZ char=Z int=90
TokNext char=[ int=91

📝 Try it Yourself: Set the first enumerator to 'x' and verify the next unassigned enumerator becomes 'y' (one code point higher). Then set a later enumerator to 'z' and confirm the following one auto-increments to '{'.

1.3 Unscoped enum with explicit char underlying type

You can also specify the underlying type for an unscoped enum. With : char, enumerators must fit within the char range. When printing numeric codes, cast to int for readable output.

#include <iostream>
enum Letters : char {
    A = 'A', // fits in char
    B,       // 'B'
    C        // 'C'
};
int main() {
    std::cout << "A as char=" << static_cast<char>(A)
              << " code=" << static_cast<int>(A) << "\n";
    std::cout << "B as char=" << static_cast<char>(B)
              << " code=" << static_cast<int>(B) << "\n";
    std::cout << "C as char=" << static_cast<char>(C)
              << " code=" << static_cast<int>(C) << "\n";
}

Output (example)

A as char=A code=65
B as char=B code=66
C as char=C code=67

Note: Whether char is signed or unsigned is implementation‑defined. If you need guaranteed non‑negative codes, consider unsigned char as the underlying type.

📝 Try it Yourself: Change the underlying type to unsigned char (e.g., enum Letters : unsigned char) and print static_cast<int>(...) to verify codes remain non‑negative on your platform.

1.4 Unscoped enum: Explicit duplicates are allowed

Unscoped enums allow different enumerators to share the same value. Be cautious—this can reduce clarity in debugging and logging.

#include <iostream>
enum Suit {
    Diamonds = 5,
    Hearts,        // 6
    Clubs = 4,
    Spades         // 5 (same as Diamonds)
};
int main() {
    std::cout << "Diamonds=" << static_cast<int>(Diamonds) << "\n"
              << "Hearts="   << static_cast<int>(Hearts)   << "\n"
              << "Clubs="    << static_cast<int>(Clubs)    << "\n"
              << "Spades="   << static_cast<int>(Spades)   << "\n";
}

Output (example)

Diamonds=5
Hearts=6
Clubs=4
Spades=5

📝 Try it Yourself: Create an unscoped enum for weekdays where both Sat and Sun share the same value (e.g., 6). Print and confirm the duplicate mapping.

2. Scoped enum class (Type-Safe)

enum class is scoped and strongly typed: no implicit conversion to int, and names don’t pollute the surrounding scope. This prevents accidental comparisons or assignments with unrelated enums or integers.

#include <iostream>
enum class Direction { Up, Down, Left, Right };
int main() {
    Direction d = Direction::Left;
    // if (d == Left) ... // ERROR: must scope with Direction::
    if (d == Direction::Left) std::cout << "Go left\n";
}

Output

Go left

📝 Try it Yourself: Write a function that takes a Direction and returns a human-readable string (e.g., “Up”, “Down”, …). Call it for each enumerator.

3. Underlying Types and Explicit Values

You can choose the underlying integer type (e.g., std::uint8_t) and assign explicit values. This is useful for serialization, bit patterns, or interfacing with hardware and protocols.

#include <cstdint>
enum class Status : std::uint8_t {
    Ok = 0,
    Warning = 1,
    Error = 2
};

📝 Try it Yourself: Create a Permissions unscoped enum with values Read = 1, Write = 2, Exec = 4. Combine them using bitwise OR and print the numeric result.

4. switch with enum

switch pairs naturally with enums. With enum class, use fully-qualified enumerators. This improves clarity and helps the compiler catch missing cases.

#include <iostream>
enum class Mode { Read, Write, Append };
int main() {
    Mode m = Mode::Write;
    switch (m) {
        case Mode::Read:   std::cout << "Reading\n"; break;
        case Mode::Write:  std::cout << "Writing\n"; break;
        case Mode::Append: std::cout << "Appending\n"; break;
    }
}

Output

Writing

📝 Try it Yourself: Add a new Mode::Truncate and handle it in the switch. Then try omitting a case to see your compiler’s warnings with -Wswitch enabled.

5. Bit Flags with Enums (Unscoped example)

Use unscoped enums for simple bit flags (or create helper operators for enum class). You can combine flags with bitwise OR and test with bitwise AND.

#include <iostream>
enum Permission {
    P_Read  = 1 << 0, // 1
    P_Write = 1 << 1, // 2
    P_Exec  = 1 << 2  // 4
};
int main() {
    int userPerms = P_Read | P_Exec; // combine
    if (userPerms & P_Read)  std::cout << "Has read\n";
    if (userPerms & P_Write) std::cout << "Has write\n";
    if (userPerms & P_Exec)  std::cout << "Has exec\n";
}

Output (example)

Has read
Has exec

📝 Try it Yourself: Add a P_Admin flag as 1 << 3, grant it to the user, and verify detection with (userPerms & P_Admin).

Best Practices

  • Prefer enum class for type safety and scoping.
  • Specify underlying types when size matters (I/O, ABI, bit-packing).
  • Use switch to exhaustively handle enum states (enable warnings).
  • For bit flags, consider unscoped enums or define bitwise operators for enum class.

Frequently Asked Questions (FAQ)

Q: When should I use enum class instead of enum?
A: Prefer enum class for type safety and scoping. Use classic enum only when implicit int conversions or legacy APIs require it.

Q: Can I iterate over enum values?
A: Not directly. Create an array of enumerators, or use helper utilities. For enum class, you typically define custom iteration support.

Q: How do I print an enum class value?
A: Provide a mapping function (e.g., to_string(Direction)) or overload operator<< that converts to a string or uses static_cast<int> when numeric output is acceptable.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.