When a program has to choose one branch out of many based on a single discrete value, an if/else if ladder gets noisy fast. A switch statement is C++'s purpose-built tool for that pattern: one expression, many possible values, one matching branch. This lesson covers the syntax, the types switch accepts (and the ones it refuses), the fallthrough behavior, the [[fallthrough]] attribute, declaring variables inside cases, the C++17 initializer form, and how to decide between switch and if/else.
A switch takes one expression in parentheses and compares its value against a list of case labels. The matching label is the entry point. Execution then runs forward through the body until it hits a break (or falls off the end of the switch).
The pieces line up like this:
switch (orderStatusCode) is evaluated once.case X: is a label. If the expression equals X, control jumps to that label.break ends the switch. Without it, execution would continue into the next case.default: is the fallback. If no case matches, that's where execution lands.A case label is not a separate scope on its own. It is a marker inside one big block. That detail matters when declaring variables inside cases, covered below.
switch only works on values of an integral or enumeration type. That covers int, short, long, long long, char, bool, plain enum, and enum class. It does not work on std::string, double, float, or any user-defined class.
The reason comes from how switch is implemented. The compiler is allowed (and often does) translate a switch into a jump table: an array of code addresses indexed by the case value. That only works when the labels are compile-time integer constants and the input is an integer that can be hashed or indexed directly. Floating-point values cannot be compared for equality safely (rounding errors make 0.1 + 0.2 != 0.3), and strings need character-by-character comparison, which is the opposite of a single integer jump. So C++ refuses both at the language level.
charchar is a small integer, so it works cleanly. This suits menu-style code where the user types a single letter.
Each case label is a char literal, which is a small integer constant. The compiler accepts this.
enum and enum classSwitching on enums is one of the cleanest uses of switch. The cases name themselves, and the compiler can warn about missing cases.
An enum class value cannot be compared to a plain int without a cast, and that strictness extends to switch. The case labels must be OrderStatus::PLACED and friends, not 0, 1, 2, 3. To get the underlying integer (to print it, log it, or send it across a wire), cast back explicitly with static_cast:
When a switch on an enum class covers every enumerator without a default, most compilers (g++, clang++) warn if a new enumerator is added later and not handled. That is one of the strongest reasons to switch on enums.
switch RefusesThe following will not compile.
Floats:
switch is built around exact integer equality. Floating-point numbers are stored as binary approximations of decimal values, so two doubles that "should" be equal often aren't. The language sidesteps that whole problem by rejecting float switches outright. Use if/else if with a tolerance check (std::abs(a - b) < 1e-9) instead.
Strings:
A std::string is a class with member functions, not an integer. There is no way to build a jump table from one. To dispatch on string values, use an if/else if chain, a std::unordered_map<std::string, std::function<void()>>, or map the strings to an enum first and switch on that.
Every case label is a jump target. After landing on a label, execution runs forward through whatever statements come next, even crossing into the next case label, until it hits a break or the closing brace of the switch.
That behavior is called fallthrough, and forgetting a break is one of the most well-known bug patterns in C and C++.
What is wrong with this code?
Execution lands on case 1:, prints Order placed, and then keeps running. There is no break, so it falls through into case 2:, prints Order shipped, and only stops there because that case ends with a break. The caller asked about a placed order and got told the order was also shipped. In a payment, shipping, or status-update system, that kind of bug can have real consequences.
The fix is mechanical: put a break at the end of every case unless fallthrough is intended.
Compilers can help. g++ -Wall -Wextra (and clang++ -Wall -Wextra) enable -Wimplicit-fallthrough, which warns on any case that flows into the next without a break or an explicit fallthrough marker. Turn it on, leave it on.
[[fallthrough]]Sometimes two cases should share the same body. The standard example is mapping a customer tier to a discount where the higher tiers also get everything the lower tiers get.
A simple version without any explicit marker:
This is fine and idiomatic. When the case label has no statements between it and the next case, the compiler treats the stack of labels as a group with one shared body. No warning is produced because there is nothing to fall through past.
The harder case is when one branch does some work and then deliberately continues into the next. That is where the [[fallthrough]] attribute, added in C++17, applies.
A Gold customer (tier 3) lands on case 3:, gets priority support, then deliberately falls through to pick up free returns and free shipping. [[fallthrough]]; is a statement that documents intent: the missing break is on purpose. It also tells the compiler to suppress its fallthrough warning for that one case.
A few details:
[[fallthrough]]; is a statement and must end with a semicolon.case or default label. Anywhere else, it is a syntax error.Without [[fallthrough]], compilers with the warning enabled flag the missing break as a likely bug. With it, the compiler accepts the intent. The reader of the code does too: the intent is right there in the source.
default Clausedefault: is the fallback label. If none of the case values match the switch expression, control jumps to default:. If there is no default and no case matches, the switch does nothing and execution continues after the closing brace.
Two practical points about default:
enum class and the compiler should warn about missing enumerators. The default is the safety net for unanticipated values. Without it, unexpected input slips through.default: anywhere in the switch (even between two cases), but readers expect it at the bottom. Putting it elsewhere is needlessly confusing.One case where default can be omitted: switching over an enum class whose enumerators are handled exhaustively. Tools like -Wswitch-enum will warn if a new enumerator is added without updating the switch, which is the desired safety check.
The trade-off is real: no default means an unmapped value (if one appears, say from a cast) falls off the bottom of the switch silently. Most teams hedge by adding default: assert(false); or by throwing an exception, so they get both the compile-time warning and a runtime safety net.
A switch body is one block. Cases are labels, not nested blocks. That has a subtle but important consequence: a variable declared inside a case without wrapping it in braces is visible to the cases below it.
This usually compiles, but it can cause errors. The biggest issue is that declaring and initializing a variable in one case and skipping past a later case label is not allowed.
What is wrong with this code?
The compiler reports:
The problem is that trackingNumber is declared inside the switch block, so its scope extends past case 1: and over case 2:. If statusCode is 2, execution jumps directly to case 2:, skipping past the initialization of trackingNumber. C++ does not allow that, because reading trackingNumber in a case where it was never initialized would be unsafe.
The fix is to give the case its own block with curly braces. That makes the variable's scope local to the case, so it does not leak across labels.
Now trackingNumber lives inside the inner block of case 1: only. Cases that do not need a local variable do not need the braces, but wrapping every case in braces for consistency is a defensible style choice. Many teams adopt that rule across the board.
C++17 added the ability to declare and initialize a variable in the switch header itself, scoped to the switch.
This is the same shape as the if initializer added in the same standard. It is a small, focused convenience: it gives the variable a precise lifetime (only the body of the switch sees it) and keeps the call site clean.
A small E-Commerce example. A function returns the current status of an order, and the dispatch should not leak the temporary into the surrounding scope.
The init-statement runs once, before the condition. The variable status exists for the body of the switch and nowhere else. Compared to the older form:
the C++17 form keeps the variable's lifetime tied to where it is actually used. That is a small win, but in code with many short switches it adds up to less noise in the surrounding scope.
The same form works with any initialization, not just auto. Multiple statements can be separated by commas, brace initialization works, and a different expression can still be passed as the condition:
That is an unusual pattern, but the flexibility is there.
switch and if/else if overlap. Anything that can be written as a switch can also be written as an if-chain, and vice versa. The choice usually comes down to readability and a small amount of performance.
The decision flow looks like this:
The headline rules:
if (price < 10) ... else if (price < 50) ...), comparing different variables, testing strings or floats, or for one or two branches.There is also a performance angle, but it is easy to overstate. When the case labels are dense and numerous, the compiler often generates a jump table: an array indexed by the switch value where each slot holds the address of the matching case. That is O(1) dispatch, regardless of how many cases there are. An if/else if chain, by contrast, is O(n) in the number of branches in the worst case. For two or three branches that difference is invisible. For dozens, it can be measurable in a hot path.
Compilers also optimize if chains aggressively, and modern CPUs predict branches well. For most application code, the performance argument is a footnote. The readability argument is what should drive the choice.
A switch with dense, contiguous integer labels often compiles to a jump table (O(1) per dispatch). Sparse or non-integer keys compile to binary searches or branching ladders instead. Do not rewrite an if/else if chain into a switch for speed without measuring first.
State machines are a fit for switch because each state is a discrete value with its own logic. The order-status state machine in the E-Commerce theme:
Each state's allowed transitions become one switch case. A function that decides whether a transition is legal:
Two cases share a body using empty-fallthrough (no statements between labels), the switch is exhaustive over the enum, and the function reads cleanly because each state's policy is written next to its label. An if/else if version would work, but the structure would be flatter and the intent less obvious.
A short program that uses several features in one place: switching on an enum class, the C++17 initializer form, intentional fallthrough for shared behavior, a default safety net, and braces around a case that declares a local.
A few things to note. The initializer in the switch header makes tier a switch-local variable. The Gold case declares bonusPoints inside its own braces because it has a local variable. Each intentional fallthrough is marked with [[fallthrough]]; so the compiler (and the next reader of the code) sees that the missing break is on purpose. And the default is at the bottom as a safety net, even though all four enumerators are handled.
10 quizzes