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 type bool (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.