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++98An 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
// 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 elsewhereExamples
Scoped vs unscoped
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++23std::to_underlying (C++23)
#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_tBitmask flags
enum class does not define bitwise operators — you must add them. A concise approach using std::to_underlying (C++23) or static_cast:
#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 setEnum-to-string
Switch-based conversion gives you a compile-time exhaustiveness check — never silently miss a new enumerator:
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:
// 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++20using enum (C++20)
Eliminates the repetitive prefix in switch arms and in class hierarchies:
// 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
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:
enum class State : uint8_t; // OK — compiler knows the size
// enum class State; // ill-formed: no underlying type knownBitwise ~ 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.