3.4.3. switch Statements

Time: 00:02:37 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)

The branching tests in switches are more limited than those available to if-statements, but they are typically compact and succinct. Switches are somewhat specialized and not the most frequently used flow of control statement, but they play an important role when needed. A basic switch statement is relatively simple and easy to understand. However, several variations can make switches complicated, tricky, and difficult to understand. Their complexity aside, all switches consist of two required elements.

  1. The switch keyword introducing a test expression
    1. The expression may be arbitrarily complex but is frequently just a single variable or a single, simple function call
    2. The expression must evaluate to an integer type (char, short, int, or long)
  2. A set of alternative paths or branches
    1. Each alternative begins with the case keyword followed by a colon and a constant integer value
    2. Zero or more statements follow the case/constant pair
    3. Each path or case generally ends with the break keyword; if it does not, then execution "falls through" (i.e., continues with) executing the statements in the following path or case
    4. An optional default case
      1. The program takes the default path when none of the other cases match the switch expression
      2. The default is typically placed at the bottom of the switch, but it may appear anywhere within the switch
Required switch elements.

Basic switch Statements

The code fragment and flowchart below illustrate a simple switch statement. Notice that each case and the default all end with break;. Strictly speaking, the break; isn't required for the last section - execution just "falls out" of the switch - but in my opinion, based on my experience maintaining code as a professional software engineer, it is good programming style to end a switch with a break.

switch (expression)
{
	case c1:
		statement0;
		statement1;
		statement2;
		break;

	case c2:
		statement4;
		statement5;
		break;

	case c3:
		statement6;
		statement7;
		statement8;
		statement9;
		break;

	case c4:
		statement10;
		statement11;
		statement12;
		break;

	default:
		statement13;
		statement14;
		statement15;
		break;
}
A complex logic diagram that details the behaviors and options of a switch statement. The case conditions or tests are individually matched from top to bottom. The <kbd>switch</kbd> executes the statements in the first <kbd>case</kbd> whose condition evaluates to true, and execution continues to the next <kbd>break</kbd> statement or execution hits the bottom of the <kbd>switch</kbd>. If a case does not end with a break, execution falls through to the next case.
A basic switch statement. The switch expression can be arbitrarily complex but is typically a single, integer-valued variable.
  1. The test expression, inside the parentheses, is evaluated and compared with each case value, beginning at the top and working downward.
    1. The expression and case values are compared for equality only - switches cannot compare values using less than, greater than, etc.
    2. The program executes the statements following the first matching case.
    3. The program continues running downward until it reaches a break statement, which ends the switch.
  2. A default section is optional.
    1. A default may appear anywhere in the switch but is usually placed at the bottom.
    2. If none of the cases match the test expression, then the default statements execute.
    3. If there isn't a default and none of the cases match the test expression, then none of the switch statements execute.
Note that c1 through c4 represent constants.

Why do I recommend ending a switch with a break? Imagine that a switch does not end with a break, and while maintaining the code, a programmer adds a new case or default at the end of the switch. If the programmer does not notice that the switch doesn't end with a break and fails to add one, the programmer inadvertently adds a potentially disastrous bug to the code. If this omission is so potentially dangerous, why is it not caught by the compiler? The answer lies at the heart of the variations that can make switch statements tricky.

Advanced Switch Statement Forms

A switch first evaluates the test expression and then compares the expression value with each case, beginning with the topmost one and working downward. When a case matches the test expression value, the program executes the associated statements. The program continues to execute code until it encounters a break statement or reaches the end of the switch statement. How does a switch behave if a case does not end with a break?

		.
		.
		.
case c2:
	statement0;
	statement1;
	statement2;
	// fall through
case c3:
	statement3;
	statement4;
	break;
		.
		.
		.
A logic diagram that illustrates the execution path when a case does not have a break.
A case fall through. A fragment of a more complex switch statement: case c2 does not end with a break, which means that whenever the code in case c2 runs, the code in case c3 will also run. Forgetting a break statement is a common programming error, so it's a good idea to add a comment when intentionally omitting the break.

The following example is a special case of the advanced "case fall through" example above. This form is used often but is also very easy to understand. The program treats two or more adjacent cases as a logical-OR.

		.
		.
		.
case c2:
case c3:
	statement0;
	statement1;
	break;
		.
		.
		.
A logic diagram illustrating two adjacent cases, which we read as OR. So, the statement following case c3 executes if either case c2 or case c3 is true.
An implied logical-OR. Two or more consecutive cases without intervening statements behave like and read as a logical-OR: exp == c2 || exp == c3

Scope: Defining Variables In A Case

A case does not create a new scope. The only time that this situation is a concern is when we need or want to define variables inside a case.

switch (expr)
{
    case c1:
        int    counter;
        . . .
        break;

    case c2:
        int    counter;
        . . .
        break;
    .  .  .
}




switch (expr)
{
    case c1:
    {
        int    counter;
        . . .
        break;
    }

    case c2:
    {
        int    counter;
        . . .
        break;
    }
    .  .  .
}
int    counter;

switch (expr)
{
    case c1:
        . . .
        break;

    case c2:
        . . .
        break;
   .  .  .
}




(a) Does Not Compile (b) Compiles (c) Compiles
Defining variables in a case (part 1).
  1. It's always an error to define two variables with the same name in the same scope (or to define the same variable more than once, however you want to look at it)
  2. Adding braces so that each case forms a unique scope solves the problem. We can place the closing brace before or after the break
  3. It's often easier to hoist the variable definition outside and above the switch

Defining different variables (with different names) in each case can create situations with subtle, puzzling behavior.

switch (expr)
{
    case c1:
        int    min;
        . . .
        break;

    case c2:
        int    max;
        . . .
        break;
    .  .  .
}
switch (expr)
{
    case c1:
        int    min = 100;
        . . .
        break;

    case c2:
        int    max = 200;
        . . .
        break;
    .  .  .
}
(a) Compiles (b) Does Not Compile
Defining variables in a case (part 2).
  1. Although some compilers can handle different variable definitions in the same scope, this approach is fragile, may not be portable, and is not recommended
  2. Defining and initializing a variable with a single statement in a case in a one-scope switch causes most compilers to fail with a compile-time error. The reason is that it's possible to skip an initialization when the program runs. In this example, the program may skip case c1, leaving min uninitialized. This situation is sufficient to cause a fatal compile-time error even if min is unused in other cases

Variable initializations are crucial operations. Consequently, the compiler ensures that they occur as intended. As a result, Figure 6(b) produces a fatal error that aborts the compilation of the program because it is possible to skip the initialization. On the other hand, Figure 7(a) accomplishes the same result but with a separate initialization operation. So, what's the difference? Figure 6(b) is a compile-time initialization, and Figure 7(a) is a run-time initialization. Compile-time initialization is more efficient but less flexible. Nevertheless, creating separate scopes for each variable, as illustrated in Figures 5(b) and 7(b), provides the safest and the most robust solution.

switch (expr)
{
    case c1:
        int    min;
        min = 100;
        . . .
        break;

    case c2:
        int    max;
        max = 200;
        . . .
        break;
    .  .  .
}


switch (expr)
{
    case c1:
    {
        int    min = 100;
        . . .
        break;
    }

    case c2:
    {
        int    max = 200;
        . . .
        break;
    }
    .  .  .
}
(a) Correction: Separate Initialization (b) Correction: Multiple Scopes
Defining variables in a case (part 3).
  1. Separating the definition from the initialization is sufficient (at least in Visual Studio) to complete the compilation process, but it still seems fragile and may not be portable
  2. Creating a separate scope for each case provides a more robust solution

Examples And Elaboration

Research comparing excellent adult learners with less capable ones also confirmed that the most successful learners elaborate what they read and construct explanations for themselves... Students were classified on the basis of their performance on a test given after they studied a chapter in a physics text, and of their activities during learning as they studied the example problems. The better students treated the examples quite differently, constructing explanations of solutions in terms of problem goals and physics principles discussed in the texts, rather than simply attending to the sequence of steps in solutions, as the poorer students tended to do. (Greeno, Collins, & Resnick, 1996, p. 19).

To elaborate means "to add details in writing, speaking, etc.; give additional or fuller treatment." Previous examples provide detailed descriptions of what each statement in the example does. These details are my elaboration of the example. Elaborations help us understand concepts sufficiently well that we can use them to solve problems that we haven't seen before. Elaboration is also a skill that you can and must develop through practice. Study the two following examples until you can describe the behavior of each, line by line, to someone else. Subsequent sections present completed examples demonstrating switch-statements: temp.cpp (switch), temp3.cpp, and wc.cpp.

char	c;
	.
	.
	.
switch (c)
{
	case 'y' :
		cout << "Yes" << endl;
		break;
	case 'n' :
		cout << "No" << endl;
		break;
	default  :
		cout << "Unknown response" << endl ;
		break;
}




int	i = 1;
switch (i)
{
	default   :
		cout << "sixth\n";
		break;
	case 0    :
		cout << "first\n";
		break;
	case 1    :
		cout << "second\n";
	case 10   :
		cout << "third\n";
	case 100  :
		cout << "fourth\n";
		break;
	case 1000 :
		cout << "fifth\n";
		break;
}
 
second
third
fourth
switch-statement examples. The first example demonstrates a three-way branch including the default keyword. In the second example, notice which cases have a break statement and which do not. It is not a syntax error to have a case without a break, but it does change the behavior of the switch statement. Also, notice the placement of the default case. default often appears at the bottom of the default, but the C++ syntax allows it anywhere in the switch. The output produced by the second switch appears below it in the table. Ensure you can explain why the switch statement produces this output.


Greeno, J. G, Collins, A. M., & Resnick, L. B. (1996). Cognition and learning. In D. C. Berliner & R. C. Calfee (Eds.), Handbook of educational psychology (pp. 15-46). New York: Macmillan.