Scoped enums (enum class)

C++ · Practical · 8 min

Enums replace magic numbers and bool parameters; enum class does it without the name clashes and silent conversions of old enums.

An enumeration is a type with a fixed set of named values. C++ has two kinds.

The old C-style enum Color { Red, Green }; puts Red and Green directly into the surrounding scope (so two enums can't both have None) and converts silently to int, so Red + 3 and if (color == 1) compile.

The C++11 scoped enum, enum class Level { Debug, Info, Warn, Error };, fixes both problems:

  • Values are scoped: you write Level::Warn.
  • No implicit conversion to integers; use static_cast<int>(l) when you need the number, or C++23's std::to_underlying(l).
  • Comparisons between values of the same enum still work: l >= Level::Warn.

You can fix the underlying type, enum class Level : std::uint8_t, to control size in structs and binary formats; for scoped enums it defaults to int. Values count up from 0 unless you assign them explicitly (Error = 40). Enums with a specified underlying type can be forward-declared.

switch on an enum is the natural way to handle each case. If the switch has no default, -Wall warns when you forget an enumerator, which is a useful safety net when someone adds a new value later. C++20's using enum Level; brings the names into a scope so you can write case Warn: instead of case Level::Warn:.

Use enum class for states, modes and options instead of int codes or several bool parameters: open(path, Mode::ReadOnly) reads far better than open(path, true, false). There's still no built-in enum-to-string conversion; write a function with a switch.

Example

#include <cstdint>
#include <iostream>

enum Color { Red, Green };                      // old style: names leak, converts to int
enum class Level : std::uint8_t { Debug, Info, Warn, Error };   // scoped, one byte

const char* label(Level l) {
    using enum Level;                           // C++20: no Level:: prefix needed below
    switch (l) {                                // -Wall warns if a case is missing
        case Debug: return "debug";
        case Info:  return "info";
        case Warn:  return "warn";
        case Error: return "error";
    }
    return "?";
}

int main() {
    Level l = Level::Warn;
    // int bad = l;                             // error: no implicit conversion
    int n = static_cast<int>(l);                // 2 (C++23: std::to_underlying(l))
    bool loud = l >= Level::Warn;               // same-enum comparison is fine
    int c = Green;                              // compiles: unscoped enums convert silently
    std::cout << label(l) << ' ' << n << ' ' << loud << ' ' << c << '\n';  // warn 2 1 1
}

Remember

  • Prefer enum class: scoped names, no implicit int conversion
  • Choose the underlying type with : std::uint8_t when size matters
  • Switch without default so the compiler flags missing cases

Watch out for

  • Adding a default: to every enum switch, which silences the warning that would tell you about a newly added enumerator.

Check yourself

  1. Given `enum class Dir { Up, Down };`, which line compiles?
  2. Why might you write `enum class Opcode : std::uint8_t`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/enum

Builds on

Unlocks

Nothing builds on this yet.