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, and using enum (C++20).

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

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)

cpp
#include <utility>

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

uint32_t raw = std::to_underlying(Permission::Read);  // C++23: 1

// Pre-C++23 equivalent
uint32_t raw2 = static_cast<uint32_t>(Permission::Read);

// underlying_type_t — C++14
using T = std::underlying_type_t<Permission>;  // uint32_t

Bitmask flags

enum class does not define bitwise operators — you must add them. A concise approach using std::to_underlying (C++23) or static_cast:

cpp
#include <utility>

enum class Perm : uint32_t {
    None    = 0,
    Read    = 1u << 0,
    Write   = 1u << 1,
    Execute = 1u << 2,
    All     = Read | Write | Execute,
};

// C++11: define operators via static_cast
// C++23: std::to_underlying is cleaner
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 {
    return static_cast<Perm>(~std::to_underlying(a));
}
constexpr bool has_flag(Perm set, Perm flag) noexcept {
    return (set & flag) == flag;
}

Perm p = Perm::Read | Perm::Write;
has_flag(p, Perm::Read);    // true
has_flag(p, Perm::Execute); // false

Perm rw_only = p & ~Perm::Execute;  // strip execute bit if set

Enum-to-string

Switch-based conversion gives you a compile-time exhaustiveness check — never silently miss a new enumerator:

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 "Unknown";  // unreachable if Status is exhaustive
}

With std::format (C++20), wire in a custom formatter:

cpp
// C++20
template <>
struct std::formatter<Status> : std::formatter<std::string_view> {
    auto format(Status s, std::format_context& ctx) const {
        return std::formatter<std::string_view>::format(to_string(s), ctx);
    }
};

std::println("{}", Status::Active);   // "Active" — C++23 std::println
std::format("{}", Status::Closed);   // "Closed" — C++20

using enum (C++20)

Eliminates the repetitive prefix in switch arms and in class hierarchies:

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

// Selective import
void process(Signal s) {
    using Signal::Red;  // only Red is imported
    if (s == Red) reset();
}

// In derived class: expose base enum without scope noise
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) { /* ... */ }
    }
};

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

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

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's 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 (with -Wall or /W4) warns on an unhandled enumerator. With default, new values silently fall through — you lose the exhaustiveness check. If a truly unreachable path must be suppressed, use [[unlikely]] or a separate out-of-range guard after the switch.

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.


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.

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 pattern static_cast<T>(~std::to_underlying(a)) handles this correctly; writing it inline without to_underlying 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