Why this matters
In a switch, a break ends the branch. Without a break, the program keeps going into the next case and runs its code too. Beginners usually see this as a bug, and most of the time it is one. Sometimes, though, it is the design you want. Two inputs may need the same action. A higher level may get everything a lower level gets, plus something extra. In those cases a missing break is the right code, and the program is only correct if you write it on purpose.
This lesson shows two patterns. The first is stacking, where several labels lead to one block of code. The second is a deliberate fall-through, where one block of code runs into the next. For the second pattern, you mark the spot with [[fallthrough]], so that the reader and the compiler can tell the difference between an intended fall-through and a forgotten break.
This lesson assumes you know what switch, case and break do. The other lessons in this module cover the decision tables and the other ways to pick a branch.
The idea in plain words
Two terms are used in this lesson. Both are plain descriptions of what the program does.
Stacking means that several case labels appear one after another with no code between them. Each of those labels leads to the same code. A program that reads a day number can say "1, 2, 3, 4 and 5 all mean a weekday" by writing five labels above one block.
Fall-through means that a case with code in it has no break at the end, so control moves into the next case and runs its code. The code under the next label runs as well. This is the case that causes trouble when it happens by accident.
The two are easy to mix up. Stacking has no code between the labels, so nothing runs in the first label. Fall-through has real code in the first label, and that code is followed by the next label. Stacking never produces a compiler warning. With -Wextra on, fall-through does, unless you mark it.
Stacking labels for shared code
Start with the simplest case. A program reads a day number from 1 to 7. Days 1 to 5 are weekdays, and days 6 and 7 are the weekend. The program should print one message for each group.
#include <iostream>
using namespace std;
int main() {
int day;
cin >> day;
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
cout << "weekday: office open" << endl;
break;
case 6:
case 7:
cout << "weekend: office closed" << endl;
break;
default:
cout << "not a day number" << endl;
break;
}
return 0;
}Compile it with warnings turned on, and run it three times:
$ g++ -std=c++20 -Wall -Wextra -o stack stack.cpp
$ echo 6 | ./stack
weekend: office closed
$ echo 1 | ./stack
weekday: office open
$ echo 9 | ./stack
not a day numberThe compiler prints nothing, which is what you want. Here is how the program reads the code when the input is 1. The value 1 matches the first label. That label has no code under it, so control moves to the next line, which is another label. The lines under case 5: run, and the break ends the switch. Each of the labels 1 to 5 is a separate entry point into the same block.
Notice that the labels 1 to 4 have no break after them. This is correct. An empty label does not count as a fall-through, because there is no code to fall out of. You can stack as many labels as you like without writing any extra words.
The list of labels can also be written on one line, as in case 1: case 2: case 3:. Many people prefer one label per line, because it is easier to add or remove a value later. Both forms do exactly the same thing.
Marking a fall-through
Stacking is the simple case. The harder case is a block of code that should run, and then run the next block as well.
A common example is a membership program. A gold member gets the gold perks, and also everything a silver member gets. A silver member gets the silver perks, and also everything a bronze member gets. The code is easiest to write as one long chain of cases, where each higher level falls into the lower level below it.
In C++17 and later, you mark that intended fall-through with the attribute [[fallthrough]];. An attribute is a note that you put in double square brackets. It changes no behaviour in the program. It only tells the compiler, and the next reader, that the missing break is deliberate. The statement must go directly before the next case or default label, and it ends with a semicolon.
Here is the membership example. The input is a level number: 3 for gold, 2 for silver, 1 for bronze, and 0 for no membership. The discount is added up as the program falls through.
#include <iostream>
using namespace std;
int main() {
int level;
cin >> level;
int discount = 0;
switch (level) {
case 3:
cout << "gold: priority support" << endl;
discount += 10;
[[fallthrough]];
case 2:
cout << "silver: free shipping" << endl;
discount += 5;
[[fallthrough]];
case 1:
cout << "bronze: newsletter" << endl;
discount += 2;
break;
default:
cout << "no membership" << endl;
break;
}
cout << "discount: " << discount << "%" << endl;
return 0;
}The output for each input is:
$ echo 3 | ./tiers
gold: priority support
silver: free shipping
bronze: newsletter
discount: 17%
$ echo 2 | ./tiers
silver: free shipping
bronze: newsletter
discount: 7%
$ echo 0 | ./tiers
no membership
discount: 0%Trace the input 2 by hand. The value 2 matches case 2:. The program prints the silver line and adds 5 to the discount. The [[fallthrough]]; is there, so control moves into case 1:. That block prints the bronze line and adds 2, which makes 7. The break then ends the switch. The total of 7 is the sum of the silver and bronze values, and that sum is what the top-level rule is meant to produce.
Each [[fallthrough]]; is a claim. It says "the next block should run now, and this is intended." If you later move a block, add a new case in the middle, or copy the code, you can check each claim by reading the line directly above it.
A third example with a mixed chain
The next example is harder because it mixes stacking and fall-through in one switch. A small text editor reads one letter as a command. The letter s saves the file. The letter x saves the file and then exits. The letter q exits without saving. Both upper and lower case letters should work.
The command x needs to do two things: save, then exit. Exiting is the same action as the q command, so x should fall through into the exit block. The save block is the place where that happens.
#include <iostream>
using namespace std;
int main() {
char cmd;
cin >> cmd;
switch (cmd) {
case 's':
case 'S':
cout << "saved" << endl;
break;
case 'x':
case 'X':
cout << "saving before exit" << endl;
[[fallthrough]];
case 'q':
case 'Q':
cout << "exiting" << endl;
break;
default:
cout << "unknown command" << endl;
break;
}
return 0;
}Notice how each part is written:
case 's':andcase 'S':are stacked, so both letters save.case 'x':andcase 'X':are stacked, and then a block of code runs and falls through into the exit block.case 'q':andcase 'Q':are stacked. Nothing falls into them from above except through the marked fall-through.- The
breakafter the exit block stops the program from reachingdefault:.
The output for each command is:
$ echo X | ./keys
saving before exit
exiting
$ echo s | ./keys
saved
$ echo q | ./keys
exiting
$ echo z | ./keys
unknown commandWork through the input X by hand. The value matches the label case 'X':. That label has the stacked case 'x': above it, which has no code, so both letters lead to the same block. The program prints saving before exit. The [[fallthrough]]; moves control into the q group, which prints exiting. The break ends the switch.
You can read the switch with a simple method. First find the label that matches the input. Then read downward and note each line of code you pass. Stop at the first break. The list of lines you noted is the output. This method works for any switch, including the one in the example above.
Reading a switch with fall-through
The method above can be written as a small table. Each row is one input. Each column tells you which blocks run, in order. The table below shows the membership example.
| Level | Blocks that run | Discount |
|---|---|---|
| 3 | gold, silver, bronze | 17 |
| 2 | silver, bronze | 7 |
| 1 | bronze | 2 |
| 0 | default | 0 |
The table makes one thing easy to see. Levels 3 and 2 run more blocks than they match. The extra blocks come from the fall-through, not from a second check in the code. If you change the order of the cases, the table changes too. A switch with fall-through is a list of rules that depend on order, so the order is part of the design.
Common mistakes
The most common mistake is a break that was forgotten by accident. The program still compiles, and it still gives an answer, but the answer is wrong. The next example has no break and no marking after the silver block. The intent was that silver prints only its own line.
#include <iostream>
using namespace std;
int main() {
int level;
cin >> level;
switch (level) {
case 2:
cout << "silver" << endl;
case 1:
cout << "bronze" << endl;
break;
default:
break;
}
return 0;
}Compile it with -Wall only, and the compiler says nothing:
$ g++ -std=c++20 -Wall -c nomark.cpp -o /dev/null
$ echo $?
0The program runs and prints two lines for the input 2, which is the wrong result:
$ echo 2 | ./nomark
silver
bronzeThe reason the compiler stays quiet is that -Wall does not include the check for implicit fall-through. That check is part of -Wextra, so this lesson compiles with both flags. With -Wextra the compiler reports the problem:
$ g++ -std=c++20 -Wall -Wextra -c nomark.cpp -o /dev/null
nomark.cpp: In function ‘int main()’:
nomark.cpp:9:33: warning: this statement may fall through [-Wimplicit-fallthrough=]
9 | cout << "silver" << endl;
| ^~~~
nomark.cpp:10:9: note: here
10 | case 1:
| ^~~~The warning points at the line that falls through, and the note points at the label it falls into. The warning is a question from the compiler: did you mean this? If the answer is yes, add [[fallthrough]]; and the warning goes away. If the answer is no, add the break.
A second mistake is putting [[fallthrough]]; in the wrong place. The attribute has to be directly before a case or default label. If it is in the middle of a block, or after the last statement with no label after it, the compiler points it out. This sample has the attribute in front of a statement instead of a label:
#include <iostream>
using namespace std;
int main() {
int level;
cin >> level;
switch (level) {
case 2:
[[fallthrough]];
cout << "silver" << endl;
break;
case 1:
cout << "bronze" << endl;
break;
}
return 0;
}The compiler reports it under -Wall:
badmark2.cpp: In function ‘int main()’:
badmark2.cpp:9:28: warning: attribute ‘fallthrough’ not preceding a case label or default label
9 | [[fallthrough]];
| ^Fix the placement so the attribute sits right above case 1:, or delete it if the fall-through was not intended. A marker that sits in the wrong place says nothing about the code around it.
A third mistake is thinking that a stacked label is a fall-through, and then adding a [[fallthrough]]; to it. For example, writing case 'a': [[fallthrough]]; case 'e': with no code in the first label. The attribute is allowed there, but it does nothing, and it makes the next reader wonder what code is missing. Use plain stacking when the first label has no code.
A fourth mistake is marking every fall-through, including the ones that were never meant to happen. The marker is a claim about the design. If you add [[fallthrough]]; to make the warning disappear without checking the intent, you have hidden a bug. Read each marker and ask whether the next block should run after this one. If the answer is no, add a break.
Practice
Write each program, compile it with -std=c++20 -Wall -Wextra, and test each input before you read the answer. Write the expected output first.
- Read one character. Print
vowelif it isa,e,i,ooru, in lower case or upper case. For any other character, printconsonant. Use stacked labels and onebreakfor the vowel group.
Show the answer
#include <iostream>
using namespace std;
int main() {
char letter;
cin >> letter;
switch (letter) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
case 'A':
case 'E':
case 'I':
case 'O':
case 'U':
cout << "vowel" << endl;
break;
default:
cout << "consonant" << endl;
break;
}
return 0;
}The output for a, E and k is vowel, vowel and consonant.
- Extend the membership example with a level 4, platinum. Platinum gets the airport lounge line and 20 extra discount points. Platinum should also get all the gold, silver and bronze lines, and the discount should be the sum of all the points. Mark every fall-through with
[[fallthrough]];. For input 4, the discount should be 37.
Show the answer
#include <iostream>
using namespace std;
int main() {
int level;
cin >> level;
int discount = 0;
switch (level) {
case 4:
cout << "platinum: airport lounge" << endl;
discount += 20;
[[fallthrough]];
case 3:
cout << "gold: priority support" << endl;
discount += 10;
[[fallthrough]];
case 2:
cout << "silver: free shipping" << endl;
discount += 5;
[[fallthrough]];
case 1:
cout << "bronze: newsletter" << endl;
discount += 2;
break;
default:
cout << "no membership" << endl;
break;
}
cout << "discount: " << discount << "%" << endl;
return 0;
}For input 4 the output is four lines of text followed by discount: 37%. For input 1 the output is the bronze line and discount: 2%.
- The program below is meant to print
OKfor 200,Not foundfor 404, andServer errorfor 500. It prints the wrong text for 200. Find the bug, and say which line is missing its marker orbreak. Then fix it.
#include <iostream>
using namespace std;
int main() {
int code;
cin >> code;
switch (code) {
case 200:
cout << "OK" << endl;
case 404:
cout << "Not found" << endl;
break;
case 500:
cout << "Server error" << endl;
break;
default:
cout << "Other" << endl;
break;
}
return 0;
}Show the answer
The case 200: block has no break and no [[fallthrough]];. Input 200 prints OK and then Not found. The fix is to add a break; after the OK line. With that line added, input 200 prints only OK.
- Compile the program below with
-Wall -Wextra. Say which line the warning points to, and say whether the program prints the right output for input 2 when the intent is to printtwoonly.
#include <iostream>
using namespace std;
int main() {
int level;
cin >> level;
switch (level) {
case 2:
cout << "two" << endl;
case 1:
cout << "one" << endl;
break;
default:
cout << "other" << endl;
break;
}
return 0;
}Show the answer
The warning points at line 9, the cout << "two" << endl; line, and the note points at case 1:. Input 2 prints two and then one, which is wrong for this intent. The fix is either a break; after the two line, or [[fallthrough]]; if the two lines really should both print.
- Trace this program by hand for the inputs
q,xandZ. Write the output for each input before you run it.
#include <iostream>
using namespace std;
int main() {
char cmd;
cin >> cmd;
switch (cmd) {
case 'x':
case 'X':
cout << "save" << endl;
[[fallthrough]];
case 'q':
case 'Q':
cout << "quit" << endl;
break;
default:
cout << "unknown" << endl;
break;
}
return 0;
}Show the answer
For q the output is quit. For x the output is save and then quit. For Z the output is unknown. Each answer follows from the rule that a label runs its block, and then the next block runs only if the block ends without a break.
Summary
- Stacking puts several
caselabels above one block of code, so each value leads to the same code. Stacking never gives a warning. - A fall-through happens when a
casewith code has nobreak, and the nextcaseruns too. - Mark each intended fall-through with
[[fallthrough]];directly above the next label. The attribute changes nothing in the program, and it tells the reader the missingbreakis deliberate. - The
-Wimplicit-fallthroughwarning is part of-Wextra, not-Wall. Compile with both flags when you write a switch. - To read a switch with fall-through, find the matching label, then read down and note each block until the first
break. - A switch with fall-through depends on the order of its cases, so keep the order when you move code.