03 - switch
switch
is also a form of conditional statement but it has different rules regarding execution - it allows a fallthrough control flow.
The fallthrough behavior comes from the fact that unlike if
or else
, a switch
statement can execute multiple sections of code based on a single condition. There is no positive/negative branch. There are only places where execution starts and stops.
Rules
The syntax is slightly different from if
:
if
requires a value of typebool
(or something contextually convertible to it),switch
requires a value of integral or enumeration type (or something contextually convertible to it).Each
case
must use a constant expression (it must be computable at compile time).constexpr
objects satisfy this requirement.
Then, the expression in switch
is evaluated once and compared for equality with cases in order of their appearance. It's not possible to make a different comparison (e.g. !=
).
Compile the following program and test how it behaves for different numbers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> int main() { std::cout << "Enter a number: "; int x = 0; std::cin >> x; switch (x) { case 3: std::cout << "you entered 3 or a higher number\n"; case 2: std::cout << "you entered 2 or a higher number\n"; case 1: std::cout << "you entered 1 or a higher number\n"; case 0: std::cout << "you entered 0 or a higher number\n"; } } |
The execution starts on first matching case and then goes through all subsequent cases (fallthrough), without further testing for a match. If you don't get it, try reordering cases so that values are no longer in descending order.
Said differently, a switch
statement has no branches but a point where execution starts.
Why case values have to be constant expressions?
Unlike if-else, switch
(at least when it was introduced in C) was not only supposed to be a shorter version of if-else but also feature a specific optimization: jump table. The goal was to have only one condition which result affects a jump instruction, which then computes new memory address based on a table build up from constant expressions.
Today, compilers also perform this optimization on if-else blocks if possible.
What does it mean that the expression inside
switch
is evaluated once?
It means that any code there will be run once. For example, switch (func())
will call the function only once, no matter how many cases are present. This is contrary to loops, where a condition is evaluated once per iteration.
Breaks
break
is where the execution stops. In other words, it disables fallthrough.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> int main() { std::cout << "Enter a number: "; int x = 0; std::cin >> x; // for 1, prints "12345" // for 2, prints "2345" // for 3, prints "345" // for 4 and 5, prints "45" // for 6, prints "6" // for anything else, does nothing switch (x) { case 1: std::cout << "1"; case 2: std::cout << "2"; case 3: std::cout << "3"; case 4: case 5: std::cout << "45"; break; case 6: std::cout << "6"; } } |
If you add a break
to every statement then switch
behaves the same way as if-else blocks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> int main() { std::cout << "Enter a number: "; int x = 0; std::cin >> x; switch (x) { case 3: std::cout << "you entered 3\n"; break; case 2: std::cout << "you entered 2\n"; break; case 1: std::cout << "you entered 1\n"; break; case 0: std::cout << "you entered 0\n"; break; } } |
Default case
You can add a default
case which will be executed if no other cases were matched (or if previous case allowed fallthrough). This is equivalent to the last else
(with no condition) in an if-else sequence.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> int main() { std::cout << "enter a number: "; int x; std::cin >> x; switch (x) { case 3: std::cout << "you entered 3\n"; break; case 2: std::cout << "you entered 2\n"; break; case 1: std::cout << "you entered 1\n"; break; case 0: std::cout << "you entered 0\n"; break; default: std::cout << "you entered something different\n"; break; } } |
Scope
While if
always introdues an inner scope the switch
does not - all cases share the same scope. This can sometimes create problems because generally[1] transfer of control is not permitted to enter the scope of a variable.
1 2 3 4 5 6 7 8 9 10 11 12 |
switch (x) { case 1: int y = 0; // initialization std::cout << y << '\n'; break; default: // compilation error: jump to default would // enter the scope of y without initializing it std::cout << "default\n"; break; } |
To fix it simply introduce a scope:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
switch (x) { case 1: { int y = 0; // initialization std::cout << y << '\n'; break; } // y dies here default: // braces not necessary here but use them for consistency { std::cout << "default\n"; break; } } |
Warning: no default
Many compilers issue a warning when a switch
has no default
case (for a good reason) - usually it means that the programmer forgot to write code for when no case
matches. If you actually want to do nothing if no case is matched, simply add a default
case immediately terminated by a break
:
1 2 3 4 5 6 7 8 9 10 11 12 |
switch (x) { case 0: // ... // more cases... // this is how you silence the warning // and explicitly state that nothing should be done default: break; } |
Warning: fallthrough
In practice, fallthrough is hardly ever desirable. Even if it is, people instinctively use a separate if
earlier in the code which makes switch
unneeded. For these reasons, compilers warn when fallthrough can happen - in almost all cases it's unintended.
If you really want to do fallthrough (and silence the warning), there are 2 ways:
"fallthrough" comment (not all compilers may get it as they typically don't read comments)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
case 3: std::cout << "you entered 3 or a higher number\n"; // fallthrough case 2: std::cout << "you entered 2 or a higher number\n"; // fallthrough case 1: std::cout << "you entered 1 or a higher number\n"; // fallthrough case 0: std::cout << "you entered 0 or a higher number\n"; break; default: std::cout << "you entered a different number\n"; break; |
See https://stackoverflow.com/a/45137452/4818802 for more information.
C++17 fallthrough attribute used in a single statement alone in a place where you would normally put
break
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
case 3: std::cout << "you entered 3 or a higher number\n"; [[fallthrough]]; case 2: std::cout << "you entered 2 or a higher number\n"; [[fallthrough]]; case 1: std::cout << "you entered 1 or a higher number\n"; [[fallthrough]]; case 0: std::cout << "you entered 0 or a higher number\n"; break; default: std::cout << "you entered a different number\n"; break; |
If you have a situation where multiple cases are next to each other (without any code between them, like in the break
example) then a fallthrough without any comment/attribute between them is fine:
1 2 3 4 5 6 7 8 9 10 11 12 |
// this is fine, compilers will not warn on this case 6: case 5: case 4: case 3: case 2: case 1: std::cout << "you entered " << x << "\n"; break; default: std::cout << "invalid number\n"; break; |
Extra statement
Just like with if
, since C++17 it's possible to place an additional statement in switch
to create objects with limited scope:
1 2 3 4 |
switch (int x = user_input(); x) { // ... } |
Trivia
Because switch
has surprisingly permissive rules in regards to mixing it with other control flow statements, it's possible to cross it with a loop to create a Duff's device.
You might understand the article better after few next lessons. Anyway, this trick for a long time is not used in production code because its primary purpose (loop unrolling optimization) is already done by compilers.
Summary
Switch comes from C and features a quite unique behaviour - instead of having positive/negative branches it features execution start and stop, based on a set of possible jumps from a single source of comparisons.
all cases must use a constant expression
there can be a default case
you can only test for equality
Because of these, switch in C++ is used mostly as an alternative, shorter version of if-else blocks, most often for enumeration types. The possibility of accidental fallthrough can be a good source of bugs but most compilers warn if any case has no break. If a fallthrough is intentional, it should be stated explicitly.
Exercise
Take the pseudo-calculator from the previous lesson and replace if
statement(s) with switch
where possible.
Hint
switch
should be used for operation selection.