Skip to content
C++
Language
since C++98
Basic

Enumerations

C++ enum and enum class — scoped vs unscoped, underlying types, bitmask flags, enum-to-string, std::to_underlying, using enum (C++20), forward declarations, and zero-value pitfalls.

Enumerationsince C++98

An enumeration is a distinct integer type whose values are constrained to a named set of enumerators; enum class (C++11) additionally scopes those names and suppresses implicit integer conversion.

Overview

C++ has two enumeration forms. Unscoped enumerations (enum) have been present since C++98 and suffer from name leakage and implicit integer conversion. Scoped enumerations (enum class or enum struct, C++11) fix both problems. In new code, always reach for enum class.

C++11 also standardized the ability to explicitly specify an underlying integer type for both forms. C++20 added using enum to reduce verbosity in switch arms. C++23 added std::to_underlying to eliminate the static_cast dance.


Syntax

cpp
// Unscoped — C++98, avoid in new code
enum Direction { North, South, East, West };

// Scoped — C++11
enum class Color { Red, Green, Blue };
enum struct Status { Pending, Active, Closed };  // struct is identical to class

// Explicit underlying type — C++11 for both forms
enum class Byte : uint8_t { A = 0, B = 128, Max = 255 };
enum Flags : unsigned int { FlagA = 1, FlagB = 2 };

// Forward declaration (requires explicit underlying type) — C++11
enum class Priority : int;  // defined elsewhere

Scoped vs Unscoped: Quick Comparison

The two forms have different tradeoffs. This table summarises the key differences before the detailed examples below.

Propertyenum (unscoped)enum class (scoped)
Name leakage into enclosing scopeYes — Apple pollutes namespaceNo — only Fruit::Apple exists
Implicit conversion to intYes — compiles silentlyNo — static_cast required
Forward-declarable without underlying typeNoNo (same rule)
Forward-declarable with underlying typeYes — C++11Yes — C++11
Default underlying typeint (implementation may choose larger)Same: large enough to hold all values
Bitwise operators available automaticallyYes — implicit int conversion does itNo — must define them explicitly
Usable in using enum (C++20)YesYes

Both forms gain an explicit underlying type in C++11. The scoped form is strictly safer and should be the default choice.


Examples

Scoped vs unscoped

cpp
enum Fruit { Apple, Orange };      // C++98: Apple, Orange in enclosing scope
enum Color { Red, Green, Apple };  // error: Apple already declared above

enum class Signal { Red, Green, Blue };  // C++11: names scoped to Signal
enum class Status { Red, Active };       // OK — different scope, no conflict

Signal s = Signal::Red;   // must qualify
// Signal s2 = Red;       // error: Red not in scope
// int n = s;             // error: no implicit conversion
int n = static_cast<int>(s);  // explicit required pre-C++23

std::to_underlying (C++23)

Before C++23, extracting the underlying integer from a scoped enum required a verbose static_cast. C++23 introduced std::to_underlying in <utility>, which deduces the underlying type automatically and makes the intent unmistakable.

cpp
#include <utility>

enum class Permission : uint32_t { None = 0, Read = 1, Write = 2, Execute = 4 };

// C++23 — clean, type-deduced
uint32_t raw = std::to_underlying(Permission::Read);  // 1

// Pre-C++23 — must name the underlying type explicitly
uint32_t raw2 = static_cast<uint32_t>(Permission::Read);

// underlying_type_t — C++14, still useful for generic code
using T = std::underlying_type_t<Permission>;  // uint32_t

You need the underlying value in three common situations: serialising to a binary format or network protocol, indexing into a lookup array, and performing flag arithmetic on enum class values before the operators are defined. In all three cases, std::to_underlying is the right tool.


Bitmask flag enums

enum class intentionally suppresses bitwise operators because not all enums represent flags. You must opt in by defining the operators yourself. The pattern below uses constexpr friend functions so they are found by ADL and can appear in constant expressions.

Contrast with unscoped enums. An unscoped enum Flags : unsigned requires no operator overloading because the values decay to unsigned int on any arithmetic operation. That convenience is also the danger: you can accidentally pass an integer to a function that expects the enum type and the compiler will not stop you.

cpp
#include <cstdint>
#include <utility>  // std::to_underlying — C++23

// C++11 scoped enum with explicit underlying type
enum class Perm : uint8_t {
    None  = 0,
    Read  = 1u << 0,   // 1
    Write = 1u << 1,   // 2
    Exec  = 1u << 2,   // 4
    All   = Read | Write | Exec,  // 7
};

// Operators defined as constexpr free functions — found via ADL
constexpr Perm operator|(Perm a, Perm b) noexcept {
    return static_cast<Perm>(std::to_underlying(a) | std::to_underlying(b));  // C++23
}
constexpr Perm operator&(Perm a, Perm b) noexcept {
    return static_cast<Perm>(std::to_underlying(a) & std::to_underlying(b));
}
constexpr Perm operator^(Perm a, Perm b) noexcept {
    return static_cast<Perm>(std::to_underlying(a) ^ std::to_underlying(b));
}
constexpr Perm operator~(Perm a) noexcept {
    // Cast back to uint8_t first — ~uint8_t promotes to int, so we must mask.
    return static_cast<Perm>(static_cast<uint8_t>(~std::to_underlying(a)));
}
constexpr Perm& operator|=(Perm& a, Perm b) noexcept { return a = a | b; }
constexpr Perm& operator&=(Perm& a, Perm b) noexcept { return a = a & b; }

// Idiomatic flag check
constexpr bool has_flag(Perm set, Perm flag) noexcept {
    return (set & flag) != Perm{};  // Perm{} is value-initialised to zero
}

// Usage
constexpr Perm p = Perm::Read | Perm::Write;
static_assert(has_flag(p, Perm::Read));     // true
static_assert(!has_flag(p, Perm::Exec));    // true

Perm stripped = p & ~Perm::Write;           // strip Write bit

The (set & flag) != Perm{} idiom for flag checking is preferable to comparing against Perm::None because it works even when you haven't defined a None enumerator — Perm{} always value-initialises to zero regardless.

For comparison, the unscoped equivalent needs no operators because implicit conversion handles arithmetic:

cpp
// C++98 unscoped — operators work but type safety is lost
enum PermU : uint8_t { PRead = 1, PWrite = 2, PExec = 4 };

uint8_t mask = PRead | PWrite;  // OK: implicit conversion to int, then to uint8_t
void set_perms(PermU p);
set_perms(42);  // compiles silently — bad value, no diagnostic

using enum (C++20)

Before C++20, every switch arm over an enum class had to repeat the enum name. In a long switch this becomes noise. using enum imports all enumerators of a scoped enum into the local scope — exactly like a using namespace but limited to one enum's members.

cpp
// Pre-C++20: repetitive prefix on every arm
void render_old(Color c) {
    switch (c) {
        case Color::Red:   draw_red();   break;
        case Color::Green: draw_green(); break;
        case Color::Blue:  draw_blue();  break;
    }
}

// C++20: using enum eliminates the prefix
void render(Color c) {
    using enum Color;   // Red, Green, Blue are now directly in scope
    switch (c) {
        case Red:   draw_red();   break;
        case Green: draw_green(); break;
        case Blue:  draw_blue();  break;
    }
}

You can also import a single enumerator with using EnumType::Enumerator:

cpp
// C++20 — selective import
void process(Signal s) {
    using Signal::Red;   // only Red is pulled into scope
    if (s == Red) reset();
}

using enum is also useful in class hierarchies where a base class exposes an enum and derived classes want to use its values without the full Base::EnumType::Value qualification:

cpp
// C++20
struct Base {
    enum class Mode { Fast, Safe };
};

struct Derived : Base {
    using enum Base::Mode;  // Fast, Safe accessible without Base::Mode:: prefix
    void apply(Mode m) {
        if (m == Fast) run_fast();
        else           run_safe();
    }
};

Enum-to-string without macros

Three approaches exist, each with different tradeoffs.

1. Manual switch — simplest and exhaustiveness-checked.

cpp
constexpr std::string_view to_string(Status s) noexcept {
    switch (s) {
        case Status::Pending: return "Pending";
        case Status::Active:  return "Active";
        case Status::Closed:  return "Closed";
    }
    return "";  // unreachable if all enumerators are listed
}

With -Wswitch (GCC/Clang) or /W4 (MSVC), the compiler warns if a new enumerator is added and the switch is not updated. Do not add default: unless you want to silence that warning intentionally.

2. Array lookup table indexed by std::to_underlying — O(1) and cache-friendly for dense enums.

cpp
#include <array>
#include <string_view>
#include <utility>    // std::to_underlying — C++23

enum class Weekday : int { Mon = 0, Tue, Wed, Thu, Fri, Sat, Sun };

constexpr std::array<std::string_view, 7> kWeekdayNames = {
    "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
};

constexpr std::string_view to_string(Weekday d) noexcept {
    auto idx = std::to_underlying(d);  // C++23
    if (idx < 0 || idx >= static_cast<int>(kWeekdayNames.size()))
        return "(invalid)";
    return kWeekdayNames[static_cast<std::size_t>(idx)];
}

// Works at compile time
static_assert(to_string(Weekday::Wed) == "Wednesday");

This approach is fast but fragile when enumerator values are sparse or non-contiguous. Use it only for enums whose values start at 0 and are consecutive.

3. C++26 reflection (preview) — automatic, zero maintenance.

C++26 static reflection (std::meta) makes exhaustive enum-to-string generation possible without any boilerplate. The syntax is still being finalised but the general shape is:

cpp
// C++26 — not yet widely available
#include <meta>

template <typename E>
constexpr std::string_view to_string(E value) {
    template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
        if (value == [:e:])
            return std::meta::identifier_of(e);
    }
    return "(unknown)";
}

Until reflection lands, the switch approach is the most maintainable for small enums, and the array approach wins for hot paths over dense enums.


Initialization and zero values

The zero value is always representable, even without a zero enumerator. For both enum and enum class with an explicit underlying type, value-initialisation (E{} or E e{};) produces a zero-bit pattern regardless of whether any enumerator maps to zero. This is required by the standard — an enum can be constructed from an integer value in the range of its underlying type even if no enumerator carries that value.

cpp
enum class Status : int { Active = 1, Closed = 2 };  // no zero enumerator

Status s{};       // OK — value is 0, but no enumerator equals 0
Status t = {};    // same

if (s == Status{}) {
    // s has the "invalid" zero state — this is valid C++
}

This is a silent bug source when you assume all live values have a corresponding enumerator. A serialised zero that round-trips through Status will pass static_cast without error yet fail every enumerator comparison. Guard against it with a validation function:

cpp
constexpr bool is_valid(Status s) noexcept {
    switch (s) {
        case Status::Active:
        case Status::Closed:
            return true;
    }
    return false;  // catches zero and any other out-of-range value
}

For unscoped enums without an explicit underlying type, the zero value is still a valid bit pattern for the underlying integer, so the same concern applies. Adding a None = 0 or Invalid = 0 sentinel makes the zero state intentional and self-documenting.


Forward declarations

Forward-declaring an enum allows a header to refer to the type without pulling in the full definition — useful to break include cycles or reduce compilation coupling.

Both enum and enum class can be forward-declared in C++11 if and only if an explicit underlying type is given. Without the underlying type, the compiler cannot know the size of the type and cannot generate code that uses it by value.

cpp
// forward_decl.hpp — C++11
enum class Priority : int;   // OK: underlying type known, size is sizeof(int)
// enum class Priority;      // ill-formed: no underlying type

// Unscoped forward declaration also requires explicit type:
enum Severity : uint8_t;     // OK
// enum Severity;            // ill-formed
cpp
// priority.hpp — definition
enum class Priority : int { Low = 0, Normal = 1, High = 2, Critical = 3 };
cpp
// task.hpp — includes forward_decl.hpp, NOT priority.hpp
#pragma once
#include "forward_decl.hpp"

struct Task {
    Priority priority;  // works: size is known from the forward declaration
    int      id;
};

Forward declarations are most valuable when a class in a heavily-included header stores an enum by value. Including the full enum definition in that header pulls in whatever that definition includes; the forward declaration cuts the dependency chain.


constexpr enums and switch exhaustiveness

enum class values are usable in constexpr contexts without any extra annotation — the enum type itself is a literal type. What matters for correctness is switch exhaustiveness.

With -Wswitch (GCC/Clang default when using -Wall) or /W4 (MSVC), the compiler emits a warning if a switch over an enum class or enum does not cover every enumerator and has no default label. This is one of the most valuable free diagnostics in C++.

cpp
enum class Phase { Init, Running, Done };

// Good: no default — compiler warns if Phase gains a new enumerator
constexpr std::string_view phase_name(Phase p) {
    switch (p) {
        case Phase::Init:    return "init";
        case Phase::Running: return "running";
        case Phase::Done:    return "done";
    }
    // Unreachable for valid Phase values — but the path must still compile.
    // Use std::unreachable() (C++23) to tell the optimiser it is impossible:
    std::unreachable();  // C++23 — UB if actually reached, enables optimisation
}

Before C++23, the compiler-specific spelling was __builtin_unreachable() (GCC/Clang) or __assume(false) (MSVC):

cpp
// Pre-C++23 portable fallback
#if defined(__GNUC__) || defined(__clang__)
    __builtin_unreachable();
#elif defined(_MSC_VER)
    __assume(false);
#endif

Do not add default: break as a lazy way to silence the warning. It hides every unhandled enumerator permanently. If you genuinely want a catch-all path for out-of-range values arriving from deserialisation, name it explicitly:

cpp
constexpr std::string_view phase_name(Phase p) {
    switch (p) {
        case Phase::Init:    return "init";
        case Phase::Running: return "running";
        case Phase::Done:    return "done";
    }
    return "(unknown)";  // explicit out-of-range path — not the same as default:
}

The difference is subtle: the path after the switch only runs for values not matching any case. The compiler still warns about missing case labels, so adding a new Phase::Paused without updating the switch will produce a diagnostic even though there is a fallthrough path.


Count sentinel pattern

cpp
enum class Weekday { Monday = 0, Tuesday, Wednesday, Thursday, Friday,
                     Saturday, Sunday, Count };

constexpr auto kWeekdayCount = std::to_underlying(Weekday::Count);  // C++23: 7

// Iterate over all valid weekdays
for (int i = 0; i < kWeekdayCount; ++i) {
    auto day = static_cast<Weekday>(i);
    process(day);
}

Count sentinel overflow. If the enum's underlying type is uint8_t and you declare 256 enumerators, Count wraps to 0. Catch this at compile time:

cpp
static_assert(std::to_underlying(Weekday::Count) <= 64,
              "Too many weekdays — lookup table would overflow");

Best Practices

Always prefer enum class over enum. The scoping prevents name collisions and forces explicit casts, which catches accidental integer mixing at compile time — a real source of bugs in flag and state-machine code.

Specify the underlying type whenever size or signedness matters. Network protocols, binary file formats, and bitmask flags need exact-width types. An unspecified underlying type defaults to int for unscoped enums; for enum class the standard guarantees it is large enough to hold all enumerators, but the exact type is implementation-defined.

Do not add a default label to switch statements over an enum class. Without default, the compiler warns on an unhandled enumerator. With default, new values silently fall through. If a truly unreachable path must be suppressed, use std::unreachable() (C++23) after the switch rather than inside a default arm.

Use std::to_underlying (C++23) instead of static_cast. It expresses intent clearly and is immune to a wrong-type cast — the return type is deduced from the enum's actual underlying type.

For bitmask enums, define operators as constexpr free functions. They compose cleanly with has_flag helpers and allow usage in constant expressions (e.g., static_assert). Avoid inheriting from an integer type or defining a separate bit-manipulation template unless you control many such enums.

Always add an explicit underlying type when forward-declaring an enum. This is required by the standard and also self-documents the storage size to readers of the header.


Common Pitfalls

Mixing scoped enums with integer arithmetic. The lack of implicit conversion is a feature, but it bites when refactoring old code that passes enum values to printf, writes them to a file, or indexes an array. Always go through std::to_underlying or an explicit cast.

Zero is always a valid value even without a zero enumerator. Value-initialisation (Status s{}) produces a zero-bit enum, which may not correspond to any named enumerator. Validate incoming enum values from external sources (files, network, IPC) before use.

Count sentinel overflow. If your enum's underlying type is uint8_t and you declare 256 enumerators plus Count, Count wraps to 0 — a silent data corruption. Use static_assert(std::to_underlying(MyEnum::Count) <= kMaxExpected) to catch this.

Forward-declaring without an underlying type. Without an explicit underlying type, a forward declaration is ill-formed — the compiler cannot know the enum's size. Always pair forward declarations with an explicit type:

cpp
enum class State : uint8_t;  // OK — compiler knows the size
// enum class State;         // ill-formed: no underlying type known

Bitwise ~ on small types. ~ applied to a uint8_t under the hood promotes to int before inverting, yielding a large negative number that truncates back on static_cast. The operator~ implementation above handles this with static_cast<uint8_t>(~std::to_underlying(a)); writing it inline without that inner cast is error-prone.

Unscoped enums in headers. Even when confined to a namespace, an enum leaks its enumerators into the enclosing namespace, creating cross-translation-unit naming hazards. If legacy code requires a plain enum, at minimum scope it inside a struct or namespace.


See Also