20 topics
← Back to Quick Reference/
Topic 07
SSwwiittcchh SSttaatteemmeenntt
Fall-Through · [[fallthrough]] · enum class · Initializer · Dispatch Patterns
C++17 · Advanced ReferenceSwitch Statement
01How 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.Expression must be integral type or
enum— notstring,float, or objects. - 2.Case labels must be compile-time constant expressions.
- 3.Each case falls through to the next unless you use
break,return, orthrow. - 4.
defaultis optional but recommended — it catches unhandled values.
Common patterns
- 1.Group cases with no body between them — they share the next body.
- 2.Use
[[fallthrough]](C++17) to mark intentional fall-through. - 3.Wrap case bodies in
{}when you need to declare variables. - 4.Omit
defaultwhen switching onenum classto 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"; }
| break | Exits 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 / throw | Also stop fall-through. Common in switch-based state machines and error handlers. |
| empty case | A 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 case | Declaring 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
04enum 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 class | Scoped 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. |
| -Wswitch | Warns when an enumerator is not handled by any case. Only fires when there's no default. |
| no default | Intentionally 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
| switch | Best for integral/enum with a fixed, known set of values. Compiler can optimize to a jump table. |
| if/else chain | Needed for ranges, string comparisons, or complex conditions. Sequential — O(n) in the worst case. |
| unordered_map | Best for string keys or large sparse integer sets. O(1) average lookup, but heap allocation overhead. |
| function table | Array 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.