20 topics
/
← Back to Quick Reference
Topic 07

Switch Statement

Fall-Through · [[fallthrough]] · enum class · Initializer · Dispatch Patterns

C++17 · Advanced Reference

Switch Statement

01

How switch works

switch evaluates an integral or enum expression once, then jumps directly to the matching case label — no sequential testing like if/else if. This makes it O(1) and often compiles to a jump table or binary search, faster than a long chain of equality checks.

Rules

  1. 1.Expression must be integral type or enum — not string, float, or objects.
  2. 2.Case labels must be compile-time constant expressions.
  3. 3.Each case falls through to the next unless you use break, return, or throw.
  4. 4.default is optional but recommended — it catches unhandled values.

Common patterns

  1. 1.Group cases with no body between them — they share the next body.
  2. 2.Use [[fallthrough]] (C++17) to mark intentional fall-through.
  3. 3.Wrap case bodies in {} when you need to declare variables.
  4. 4.Omit default when switching on enum class to get exhaustiveness warnings.

The compiler may generate a jump table when cases are dense integers, or a binary search for sparse values — both beat a linear if/else chain.

switch (expression) {   // expression must be integral or enum
  case 1:
    doOne();
    break;              // ← required to stop fall-through
  case 2:
  case 3:               // grouping: same body for 2 and 3
    doTwoOrThree();
    break;
  default:              // optional; runs if no case matches
    doDefault();
}

// ── What switch can match ────────────────────────────────────
// ✅ int, char, short, long, long long (and unsigned variants)
// ✅ enum, enum class
// ✅ constexpr variables used as case labels
// ❌ std::string  ❌ float / double  ❌ std::vector
// (use if/else or a map for those)

// ── Case labels must be compile-time constants ───────────────
constexpr int TIMEOUT = 408;
switch (code) {
  case 200: return "OK";
  case 404: return "Not Found";
  case TIMEOUT: return "Timeout";  // ✅ constexpr is fine
}

Fall-Through & [[fallthrough]]

02
// Fall-through: execution continues into the next case
// when there is no break (or return/throw/continue)

switch (state) {
  case INIT:
    initialize();
    [[fallthrough]];      // ✅ C++17: explicit intent, no warning
  case RUNNING:
    tick();
    break;
  case PAUSED:
    // empty case — falls through silently (common pattern)
  case STOPPED:
    cleanup();
    break;
}

// ⚠ Implicit fall-through (no [[fallthrough]]) triggers
// -Wimplicit-fallthrough warning with -Wall
// Always annotate intentional fall-through with [[fallthrough]]

// return and throw also stop fall-through:
switch (err) {
  case 0:  return "ok";
  case -1: throw std::runtime_error("fatal");
  default: return "unknown";
}
breakExits the switch immediately. Required after every case body unless you want fall-through.
[[fallthrough]]C++17 attribute. Marks intentional fall-through — suppresses -Wimplicit-fallthrough warning.
return / throwAlso stop fall-through. Common in switch-based state machines and error handlers.
empty caseA case with no body before the next case silently falls through — no warning needed.
Compile with -Wimplicit-fallthrough. It catches every unintentional fall-through. Mark the intentional ones with [[fallthrough]] to keep the warning clean.

Initializer & Variable Declarations

03
// switch with initializer (C++17) — same as if with initializer
switch (auto status = getStatus(); status) {
  case Status::OK:
    handle(status);
    break;
  case Status::Error:
    recover(status);
    break;
}
// status is destroyed after the switch block

// ── Declarations inside switch ───────────────────────────────
switch (x) {
  case 1:
    int n = 10;    // ❌ jumps over initialization — compile error
    use(n);
    break;
  case 2:
    use(n);        // would use uninitialized n
}

// Fix: wrap in braces to scope the variable
switch (x) {
  case 1: {
    int n = 10;    // ✅ scoped to this case only
    use(n);
    break;
  }
  case 2:
    break;
}
switch (init; expr)C++17: scopes a variable to the entire switch block. Same syntax as if with initializer.
int n = x in caseDeclaring a variable in a case without braces is ill-formed if control can jump over the initialization.
case { ... }Braces create a new scope — variables declared inside are destroyed at the closing brace.
Always brace case bodies that declare variables. Jumping over a variable initialization is a compile error in C++ — the compiler will reject it, but adding braces is the clean fix.

Switching on enum class

04
enum class Color { Red, Green, Blue };

Color c = Color::Green;

switch (c) {
  case Color::Red:   std::cout << "red\n";   break;
  case Color::Green: std::cout << "green\n"; break;
  case Color::Blue:  std::cout << "blue\n";  break;
  // No default — compiler warns if a Color value is unhandled
  // (-Wswitch with GCC/Clang)
}

// ── Why omit default with enum class? ───────────────────────
// If you add a new enumerator later, -Wswitch will flag every
// switch that doesn't handle it — a free exhaustiveness check.
// Adding default silences the warning and hides the gap.

// ── scoped enum (enum class) vs unscoped enum ────────────────
enum Unscoped { A, B };     // A, B leak into surrounding scope
enum class Scoped { A, B }; // must write Scoped::A — no leakage
int x = A;                  // ✅ unscoped converts to int
// int y = Scoped::A;       // ❌ no implicit conversion
enum classScoped enum — values don't leak into the outer scope. No implicit conversion to int.
enum (plain)Unscoped — values are in the surrounding scope. Implicitly converts to int.
-WswitchWarns when an enumerator is not handled by any case. Only fires when there's no default.
no defaultIntentionally omitting default on enum class switches turns the compiler into an exhaustiveness checker.

switch vs Map vs if/else

05
// When switch is the wrong tool — use a map or array instead

// ❌ Switch with 50+ cases for a lookup table is painful to maintain
// ✅ Use std::unordered_map for string keys or non-contiguous ints

std::unordered_map<std::string, int> opcode = {
  {"add", 0x01}, {"sub", 0x02}, {"mul", 0x03},
};
int code = opcode.count(name) ? opcode[name] : -1;

// ✅ Use a function pointer / std::function table for dispatch
using Handler = std::function<void()>;
std::array<Handler, 4> handlers = {
  []{ handleA(); },
  []{ handleB(); },
  []{ handleC(); },
  []{ handleD(); },
};
if (idx < handlers.size()) handlers[idx]();

// ── When to prefer switch over if/else chain ─────────────────
// switch: integral/enum, many cases, readable case grouping
// if/else: strings, ranges (x > 10 && x < 20), complex conditions
switchBest for integral/enum with a fixed, known set of values. Compiler can optimize to a jump table.
if/else chainNeeded for ranges, string comparisons, or complex conditions. Sequential — O(n) in the worst case.
unordered_mapBest for string keys or large sparse integer sets. O(1) average lookup, but heap allocation overhead.
function tableArray of function pointers or lambdas indexed by integer. O(1), no branching, highly cache-friendly.
Prefer switch for small, dense integer/enum dispatch. For string-keyed dispatch or 50+ cases, a std::unordered_map or function table is more maintainable and just as fast.