Skip to content
VLSI Mentor

C++ Part I · Module 5 · Control Flow

switch, Case Labels and Fallthrough

An else-if chain asks a series of questions and stops at the first yes. A switch does something structurally different: it takes one value and goes straight to the label that matches it. That buys two things an else-if chain cannot offer — the value being tested appears once instead of once per branch, and the compiler can reason about the set of cases as a whole. It also costs something, and the cost is the most notorious default in the language. When a case finishes, execution does not leave the switch. It carries on into the next case, and then the next, until something stops it. That behaviour is occasionally exactly what you want and is far more often a bug, which is why every C++ style guide has a rule about it. The measurement that gives this chapter its shape is what happens when you make that mistake: compiled with the warning flags this curriculum has used since Chapter 1, the program produced no diagnostic at all. Seeing it requires a flag nobody turns on by default, and saying you meant it requires a C++17 feature most code predates.

Foundation16 min readC++Part IFundamentalsControl Flow

Part I · Module 5 · Chapter 5.3 · switch, Case Labels and Fallthrough

1. Dispatch Instead of Interrogation

Chapter 5.1 §4 built a multi-way decision by asking questions in order:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (opcode == 1)      /* ... */
    else if (opcode == 2) /* ... */
    else if (opcode == 3) /* ... */

That works and it says opcode three times. A switch says it once and jumps to the matching label:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    switch (opcode) {
        case 1:  /* ... */ break;
        case 2:  /* ... */ break;
        case 3:  /* ... */ break;
    }

The difference is not just typing. An else-if chain is a sequence of independent tests — the compiler sees three unrelated conditions that happen to be written in a row. A switch is a single dispatch on one value, and that structure is visible to the compiler, which is what lets it check things about the case list that it cannot check about a chain.

A chain asks questions until one answers yes. A switch evaluates one value and goes directly to the matching label.

2. The Shape

Azvya Education Pvt. Ltd.VLSI Mentor
opcode.cpp — complete program
#include <iostream>

static void describe(int opcode)
{
    switch (opcode) {
        case 1:
            std::cout << "read\n";
            break;
        case 2:
            std::cout << "write\n";
            break;
        case 3:
            std::cout << "erase\n";
            break;
        default:
            std::cout << "unknown opcode " << opcode << '\n';
            break;
    }
}

int main()
{
    for (int op : {1, 2, 3, 9}) describe(op);
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
read
write
erase
unknown opcode 9

Four pieces of vocabulary:

  • The controlled expression — opcode, evaluated once.
  • A case label — case 1:, a marker, not a test.
  • default: — where control goes when nothing matches. Optional.
  • break — leaves the switch. Without it, §7 happens.

The word label is the accurate one. case 1: does not compare anything; it names a place to jump to. That is why §7's behaviour follows: once you have jumped into the middle of a block, nothing marks where you should stop.

3. default Is Optional, and Omitting It Is a Decision

If no label matches and there is no default, the whole switch does nothing and execution continues after it. That is sometimes right:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    switch (command) {
        case 'r': start_read();  break;
        case 'w': start_write(); break;
        // any other character: deliberately ignored
    }

A comment like that one is worth writing, because a missing default and a forgotten default look identical.

The rule of thumb: include a default when an unexpected value means something went wrong, and use it to say so — print, log, or return an error. Silently ignoring an unrecognised value is how a bad opcode becomes a mystery three layers up.

4. Several Labels, One Body

Multiple labels can lead to the same code, with nothing between them:

Azvya Education Pvt. Ltd.VLSI Mentor
grouped.cpp — complete program
#include <iostream>

static void classify(char command)
{
    switch (command) {
        case 'r':
        case 'R':
            std::cout << command << ": read\n";
            break;
        case 'w':
        case 'W':
            std::cout << command << ": write\n";
            break;
        default:
            std::cout << command << ": unrecognised\n";
            break;
    }
}

int main()
{
    for (char c : {'r', 'R', 'w', 'W', 'x'}) classify(c);
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
r: read
R: read
w: write
W: write
x: unrecognised

This is the one form of fallthrough nobody objects to, because there is no code between the labels to skip. case 'r': immediately followed by case 'R': reads unambiguously as "either of these".

5. What a switch Accepts

The controlled expression must have an integral type — the integer types from Chapter 3.3 and the character types from Chapter 3.6 — or an enumeration type, which Chapter 9.9 covers.

That is a real restriction and it is enforced:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    double d = 1.0;
    switch (d) { case 1: break; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: statement requires expression of integer type ('double' invalid)

Floating-point values cannot be switched on, and Chapter 3.5 §4's measurement is the reason to be glad: equality on a double is not a question with a dependable answer, and a jump table built on it would be worse than an if.

std::string cannot be switched on either — a fact worth stating now because people reach for it. Chapter 9.5 introduces the type; comparing strings stays an if/else if job.

6. A case Label Must Be a Constant

The value after case has to be known at compile time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int c = 1, n = 2;
    switch (c) { case n: return 1; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: case value is not a constant expression

This is the same compile-time/run-time distinction Chapter 4.10 §7 drew. A switch is a dispatch built before the program runs, so the labels have to be available then. Chapter 3.9's const values and the constexpr of Chapter 7.6 both qualify; an ordinary variable does not.

Declarations inside a case need braces. Because the labels are jump targets, control can enter a case past a declaration that initializes something — so C++ forbids it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    switch (code) {
        case 1:
            int width = 32;     // initialization can be jumped over
            return width;
        case 2:
            return 0;
    }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: cannot jump from switch statement to this case label

Note where the error points: at case 2:, the label that would jump past width's initialization. The fix is a block, which makes the lifetime end before the next label:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    switch (code) {
        case 1: {
            int width = 32;
            return width;
        }
        case 2:
            return 0;
    }

Verified: that version compiles with zero diagnostics. Chapter 3.8 explained why the language is strict here — the alternative is an object in scope whose initializer never ran, which is exactly the uninitialized read that chapter was about.

7. Fallthrough

Now the behaviour the chapter exists for.

When a case's statements finish, execution does not leave the switch. It continues into the next case:

Azvya Education Pvt. Ltd.VLSI Mentor
fallthrough.cpp — complete program. The break on case 1 is missing.
#include <iostream>

int main()
{
    int code = 1;
    switch (code) {
        case 1:
            std::cout << "one\n";
        case 2:
            std::cout << "two\n";
            break;
        default:
            std::cout << "other\n";
            break;
    }
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
one
two

code was 1. Both messages printed. The case 2: label was not a barrier — it was just a label, and execution ran straight through it.

Now the measurement that matters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic fallthrough.cpp -o fallthrough
$                                            ← zero warnings

Nothing. No diagnostic at the warning level this curriculum has compiled every example with since Chapter 1.

The warning exists, and it is not in that set:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -Wimplicit-fallthrough fallthrough.cpp -o fallthrough
fallthrough.cpp:9:9: warning: unannotated fall-through between switch labels [-Wimplicit-fallthrough]

One extra flag, and the bug becomes visible.

8. Saying You Meant It: [[fallthrough]]

C++17 added an attribute whose only job is to record intent:

Azvya Education Pvt. Ltd.VLSI Mentor
annotated.cpp — complete program
#include <iostream>

int main()
{
    int code = 1;
    switch (code) {
        case 1:
            std::cout << "one\n";
            [[fallthrough]];
        case 2:
            std::cout << "two\n";
            break;
        default:
            break;
    }
    return 0;
}

Expected output, unchanged from §7:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
one
two

And compiled with the warning enabled:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -Wimplicit-fallthrough annotated.cpp -o annotated
$                                            ← zero warnings

Three things to notice.

It changes nothing at run time. The output is identical. The attribute is a statement for the compiler and for the next reader, not an instruction to the processor.

It needs a semicolon. [[fallthrough]]; is a statement, so it is punctuated like one.

It only tells the truth about one spot. You put it at the end of the case that falls through, not on the case being fallen into.

This is the same shape of solution as Chapter 4.10's named casts: when a construct is legitimately used two ways and the source cannot tell them apart, the fix is a spelling for the deliberate case so the accidental one becomes conspicuous.

9. The Honest Comparison with an else-if Chain

Neither construct is better in general. They fit different shapes.

switchelse if chain
Testsone value against constantsany conditions at all
Value appearsonceonce per branch
Acceptsintegral and enumeration typesanything
Rangesnoyes
Two different variablesnoyes
Falls through by defaultyesno
Compiler can check the case setyesno

Use a switch when every branch compares the same value to a different constant. Use a chain for ranges, for combined conditions, and for anything a switch cannot express.

10. What to Actually Do

End every case with break — or return, which leaves the function and therefore the switch too.

Add -Wimplicit-fallthrough to your build, since -Wall -Wextra -Wpedantic does not include it.

Annotate deliberate fallthrough with [[fallthrough]];, which is what makes the flag usable.

Group labels with nothing between them when several values share one body — the uncontroversial form.

Brace any case that declares a variable.

Include a default for an int switch when an unexpected value is an error, and say so in it.

Reach for a chain the moment you need a range, two variables, or a non-integral type.

11. Mistakes Worth Making Once

Forgetting break. Measured: prints both messages, zero warnings at -Wall -Wextra -Wpedantic.

Assuming -Wall -Wextra covers fallthrough. It does not; -Wimplicit-fallthrough is separate.

Switching on a double. error: statement requires expression of integer type.

Using a variable as a case label. error: case value is not a constant expression.

Declaring a variable in a case without braces. error: cannot jump from switch statement to this case label — and the error points at the next label.

Expecting case 2: to stop execution. It is a label, not a barrier.

Putting [[fallthrough]] on the case being entered. It belongs at the end of the case being left.

Omitting default by accident. Indistinguishable from omitting it on purpose unless you leave a comment.

12. Interview Relevance

Two questions come up, and they are not the same question.

"What happens if you forget break?" tests whether you know the behaviour. Say that execution continues into the next case and keeps going until a break, a return or the end of the switch.

"Why does the language work that way?" tests whether you know it is a feature. Deliberate fallthrough is occasionally the clearest way to express shared handling, and C++ inherited the behaviour from C, where the jump-table model made it natural. The modern answer adds that C++17's [[fallthrough]] exists precisely because the two cases needed different spellings.

The detail that distinguishes a strong answer: the default warning set does not catch it. Someone who has actually been bitten knows that -Wall -Wextra is not enough here.

13. What You Should Be Able to Say Now

  1. A switch dispatches on one value; a chain asks questions in order.
  2. case 1: is a label, not a test — which is why fallthrough follows.
  3. The controlled expression must be integral (or an enumeration type, Chapter 9.9).
  4. Measured: switching on a double is error: statement requires expression of integer type.
  5. Measured: a non-constant case label is error: case value is not a constant expression.
  6. Measured: declaring in a case without braces is error: cannot jump from switch statement to this case label; braces fix it.
  7. When a case finishes, execution continues into the next one. Measured: one then two.
  8. Measured and silent: fallthrough produces zero warnings at -Wall -Wextra -Wpedantic.
  9. -Wimplicit-fallthrough reports it: unannotated fall-through between switch labels.
  10. It is outside -Wall because the language cannot tell deliberate fallthrough from forgotten break — the same spelling means both.
  11. C++17's [[fallthrough]]; records intent, changes nothing at run time, needs a semicolon, and goes at the end of the case being left.
  12. The flag and the annotation are two halves of one tool.
  13. Grouped labels with nothing between them are the one uncontroversial fallthrough.
  14. switch gets a check on its case set that a chain cannot; for an int there is nothing to check, and Chapter 9.9 is where that pays off.

14. Exercises

Exercise 1 — trace the fallthrough. For §7's program, give the output for code = 1, 2 and 7, and say for each how many messages printed and why.

Exercise 2 — add the breaks. Fix §7's program so each code prints exactly one message, then say what the output becomes for all three inputs.

Exercise 3 — which is it? For each, say whether the fallthrough is deliberate or a bug, and what you would write: (a) case 'r': case 'R': read(); break; (b) case 1: log(); case 2: send(); break; (c) case 3: prepare(); [[fallthrough]]; case 4: send(); break;

Exercise 4 — predict the diagnostic. For each, say whether it compiles and give the diagnostic if not: (a) switch (2.0) (b) case limit: where limit is an int variable (c) case limit: where limit is a const int initialized with a literal (d) a case that declares int n = 0; with no braces.

Exercise 5 — the missing flag. A colleague says their build catches fallthrough because they use -Wall -Wextra. Say what the measurement shows, and give the exact flag they need.

Exercise 6 — chain or switch? For each, choose and justify: (a) opcode is 1, 2, 3 or anything else (b) mhz is under 100, under 1000, or more (c) command is 'r', 'R', 'w' or 'W' (d) ready is true and error is false (e) a std::string equals one of four names.

Exercise 7 — brace it. Rewrite so it compiles, and explain in one sentence what the language is protecting against:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    switch (mode) {
        case 1:
            int scale = 4;
            return scale * value;
        case 2:
            return value;
    }

15. Summary

  • A switch dispatches on one value; case labels are jump targets, not tests.
  • It accepts integral and enumeration types only — double and std::string are compile errors.
  • Case labels must be constant expressions, and a case that declares a variable needs braces.
  • Execution falls through from one case into the next by default. Measured: one then two.
  • Measured: that produces zero warnings at -Wall -Wextra -Wpedantic. -Wimplicit-fallthrough is a separate flag.
  • It sits outside -Wall because deliberate and accidental fallthrough are spelled the same.
  • C++17's [[fallthrough]]; gives the deliberate case its own spelling; flag and annotation are one tool in two halves.
  • Grouped labels with no code between them are the uncontroversial form.
  • switch exposes structure a compiler can check; Chapter 9.9's enumerations are where that check arrives.
  • Use a chain for ranges, two variables, or non-integral types.

Three chapters in, every construct has made a decision once and moved on. The rest of the module is about code that runs the same statements repeatedly, which brings a question none of these chapters had to answer: a decision either happens or it does not, but a loop has to stop. The next chapter puts that requirement at the centre — a loop is a condition plus a guarantee of progress — and shows what the language does when the guarantee is missing. The measured answer is stranger than hanging: the same program, compiled twice at different optimisation levels, behaved two different ways, and one of them was not an infinite loop at all.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.