Keyword – switch, case, default

The switch keyword is probably the least well understood of the C/C++ language keywords (although, const probably comes a close second).

The keywords switch, case, and default always go together and cannot be used independently in any other context.

There is a small difference in behaviour between C and C++. Most programmers will never notice the difference.

The most common use of switch is as a replacement for multiple if-else statements:


                               switch (value)
                               {
if (value == 1)                   case 1:
{                                      ...
  ...                                 break;
} else if (value == 2)            case 2:
{                                      ...
  ...                                 break;
} else                            default:
{                                      ...
  ...                                 break;
}                              }

This usage is quite well understood. Its use as a jump table into a block of code is less well understood (and, to be honest, rarely used).

Format

switch ( controlling expression )
{
   case: constant expression  // optional
         .        .
         code statements
         .        .
         break;  // optional
   default:  //optional
         .        .
         code statements
         .        .
         break;  // optional
}

switch (controlling expression)

Every switch block must begin with the switch keyword. If you want a switch block then you have to use the switch keyword.

C and C++ differ in the way the controlling expression is handled.

In C the expression is converted to type int using C conversion rules. This means the controlling expression can be anything C knows how to convert to an int: the signed and unsigned varieties of char, short, int, long and long long as well as float, double, and long double.

In C++ the controlling expression must be an integral type – any of the signed or unsigned varieties of char, short, int, long, or long long – or any class, struct, or union for which there exists an unambiguous conversion to an integral type. Using a float, double, or long double is an error (unless, of course, you explicitly cast it to one of the integral types).

An “implicit” conversion from a class, struct or union to an integral type might seem new to some. Here is an example:

class A
{
 private:
    int value;
 public:
    operator int() {return value;} // conversion method
};

This can be used in a switch statement as follows:


int main(void)
{
   A a;  // variable of type A above with conversion to int
   switch (a) // works because compiler calls operator int()
   {
      .
      .
      .
   }
   return 0;
}

In the above example, when an object of type A is used anywhere a type int is expected, the conversion operator int() will be called.

If we expand the class by adding another conversion operator to a different integral type, say

operator char() {return char(value);}

then it is no longer unambiguous because there are two integral types to choose from: int or char. The compile will fail because the compiler cannot resolve between the two integral types.

Conversions can be ambiguous in other types of code as well:

A a;
int i;
char c;
short s;
i = a; // compiler calls operator int()
c = a; // compiler calls operator char();
s = a; // compiler cannot resolve between int() and char()

case (constant expression)

case statements are optional – you don’t have to have any. But, you do need at least one case or default statement in your switch body.

The constant expression is called a label or case label.

Unlike the controlling expression, the case label must be known at compile time – it cannot be a variable or in any other way determined at run time.

All case labels must be different. If two case labels resolve to the same value, then the program is malformed and the compile will fail.

The two most common ways that case labels get the same value is through macros or enums:


// at one time, the following macros all had different values
// but at some point the difference between CONDITION_1 and
// CONDITION_2 was lost and they became the same.
// Most code will work just fine with this change, but not a
// switch block if it is using these labels.
#define CONDITION_1 0
#define CONDITION_2 0  // duplicated value
#define CONDITION_3 2
// instead of using macros, the conditions were encapsulated
// in an enum, but at some point the difference between CONDITION_1
// and CONDITION_2 was lost and they became the same.
// Most code will work just fine with this change, but not a
// switch block if it is using these labels.
enum Some_Values
{
   CONDITION_1 = 0,
   CONDITION_2 = 0, // duplicated value
   CONDITION_3 = 1
};
// when either the macros or enum values are used for the case
// labels, the program becomes malformed because two labels
// have the same value
switch (value)
{
   case CONDITION_1:
   case CONDITION_2: // compile will fail
   case CONDITION_3:
   default:
}

C and C++ differ in the way they handle the constant expression.

In C, the the expression is converted to type int. Both the controlling expression and constant expression are of type int.

In C++, the expression is converted to the type of the controlling expression. This means the constant expression is the same type as the controlling expression.

default

The default statement is optional – you don’t have to have one. But, you do need at least one case or default statement in your switch body.

The default statement is where the switch operator jumps to if none of the case labels match the controlling expression.

Using switch for Multiway Selection

This is the most common (and best understood) use of the switch statement.

The switch statement is most commonly used when you want to choose one of several code paths and the decision is based on some sort of integer value (an enum is an integer at heart).

For simple choices, an if-else block works fine. But when you have many possible code paths to choose from, having a long chain of if-else statements can be unwieldy.

Consider a basic DVD player that responds to the following commands:

enum PLAYER_COMMANDS
{
   Off,
   On,
   Stop,
   Play,
   Pause,
   Eject
};

Implemented using if-else statements, the code would look like this:

if (command == Off)
{
   ... code ...
}
else if (command == On)
{
   ... code ...
}
... remaining else-if code ...
else
{
   printf("Error - unknown state\n");
}

Using a switch statement, the code would look like this:

switch (command)
{
   case Off:
      ... code ...;
      break;
   case On:
      ... code ...;
      break;
   ... remaining cases ...
   default:
      printf("Error - unknown state\n");
}

The switch block looks neater and more compact than the nested if-else code.

Order of case statements

The case and default statements inside the switch don’t have to be in any particular order. While not typical, it is perfectly valid to have the default statement at the top of the block (or in the middle of the block):

switch (controlling_expression)
{
   default: // not typical location, but perfectly legal
      ... some code ...
      break;
   case label_1:
      ... some code ...
     break;
};

Use of break

Usually, a break statement is placed at just before the next case (or default) statement.

Code execution stops at the break statement and continues at the next statement following the switch block.

If there is no break statement, then the code continues to be executed until either (1) a break statement is encountered, or (2) execution exits the switch block.

switch (controlling_expression)
{
   case label_1:
      ... some code 1 ...
   case label_2:
      ... some code 2 ...
      break;
   default:
      ... some code 3 ...
};

In the above example, if the controlling_expression switches to:

  • label_1: then some code 1 will be executed. Since there is no break statement, execution will fall through and execute some code 2. Since there is a break after some code 2 code execution will continue at the next statement following the switch block.
  • label_2: then some code 2 will be executed. Since there is a break statement, code execution will continue at the next statement following the switch block.
  • default: then some code 3 will be executed. There is no break statement (it is optional for the last final label) because the next statement to be executed will be the one following the switch block.

Using Fall-through

Fall-through can be useful.

Unlike some other languages, C and C++ don’t allow a range of values for for a case label. For example, you could not write:

switch (month)
{
   case January ... July :
      ... some code ...
};

But, using fall-through, you can write code like this:

switch (month)
{
   case January:
   case March:
   case May:
   case July:
   case August:
   case October:
   case December:
      days = 31;
      break;
   case April:
   case June:
   case September:
   case November:
     days = 30;
     break;
   case February:
     days = 28;
};

In the above example, January falls through to the assignment days = 31;

Using switch as a Jump Table

This is the least commonly understood and used behaviour of the switch statement. Its most infamous example is Duff’s Device.

A common complaint of programmers coming from other languages is that the switch in C and C++ is broken – it seems dumb to have to explicitly code a break to prevent fall-through in case statements. That’s because in most languages a switch or case block is compact way of selecting a block of code to execute. This is not the case in C and C++. In C and C++, the switch statement is not for selecting a block of code to execute, it is a jump table into a block of code.

Consider the following:

switch (control)
{
   default:
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
}

No matter what value control has, the 8 printf statements will be executed. We might as well not even have the switch statement to begin with. The code is, effectively, identical to 8 printf statements enclosed in their own block:

{
   printf ("Hello world!!!\n");
   printf ("Hello world!!!\n");
   printf ("Hello world!!!\n");
   printf ("Hello world!!!\n");
   printf ("Hello world!!!\n");
   printf ("Hello world!!!\n");
   printf ("Hello world!!!\n");
   printf ("Hello world!!!\n");
}

We could also write it this way:

goto DEFAULT: // whatever the value of control
{
DEFAULT:
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
}

Consider the following (which will print Hello World!!! either 1, 2, 3, 4, 5, or 8 times depending on the value of control):

switch (control)
{
   default:
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
   case 5:
      printf ("Hello world!!!\n");
   case 4:
      printf ("Hello world!!!\n");
   case 3:
      printf ("Hello world!!!\n");
   case 2:
      printf ("Hello world!!!\n");
   case 1:
      printf ("Hello world!!!\n");
}

It can be rewritten as:

if (control == 1) goto LABEL_1;
if (control == 2) goto LABEL_2;
if (control == 3) goto LABEL_3;
if (control == 4) goto LABEL_4;
if (control == 5) goto LABEL_5;
else goto DEFAULT;
{
DEFAULT:
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
      printf ("Hello world!!!\n");
LABEL_5:
      printf ("Hello world!!!\n");
LABEL_4:
      printf ("Hello world!!!\n");
LABEL_3:
      printf ("Hello world!!!\n");
LABEL_2:
      printf ("Hello world!!!\n");
LABEL_1:
      printf ("Hello world!!!\n");
}

It should be obvious that a switch block is a constrained goto where you can jump to any line inside the block via an appropriate case label.

There is no need to get alarmed, every control statement in every programming language that alters the flow of program execution is a constrained goto.

Consider a do-while loop:

do
{
   x = x + 2;
}
while (x < 100);

This is identical to:

DO:
{
   x = x + 2;
}
if (x < 100) goto DO;

Knowing this helps to understand some of the “weird” behaviour of the switch statement in C and C++.

Fall-through

Fall through occurs because we do not select a block of code to execute, but jump to a line of code (within the block) and continue execution from there.

The break at the end of what we want to execute is simply a goto to the end of the block.

Looked at another way, switch or case constructs in other languages do exactly the same thing, except that before the next label, they implicitly insert a goto to the end of the block.

Consider the following example in Pascal:

 case A of
   0: writeln('nothing');
   1: writeln('number 1');
   2: writeln('number 2');
   else writeln('big number');
 end;

This is identical to:

if A = 0 then goto ZERO;
if A = 1 then goto ONE;
if A = 2 then goto TWO else goto BIG;
ZERO:
   writeln('nothing');
   goto CONTINUE;
ONE:
   1: writeln('number 1');
   goto CONTINUE;
TWO:
   2: writeln('number 2');
   goto CONTINUE;
BIG:
   writeln('big number');
CONTINUE:

Notice the implicit gotos inserted to prevent fall-through.

Initialization Errors

It is common to want to use some local variables in a case label. Unfortunately, doing so can generate a compile time error that the variable has not been initialized:

switch (value)
{
   int a = 7;
   case 1:
       printf("%d\n", a);
       break;
   default:
       printf("%d\n", a * a);
}

In this example, when the code jumps to case 1 or default it bypasses the initialization of a.

Jumping into the middle of and embedded switch

Since a switch statement allows you to jump to an arbitrary point in a block of code, it is reasonable to ask if it is possible to jump into the middle of a nested switch block:

switch (value)
{
   case LABEL_1:
      switch (value_2)
      {
         case OTHER_LABEL_1:
            ... code ...
            break;
         case LABEL_2:  // can switch (value) jump here?
            ... code ...
            break;
      }
   case LABEL_3:
      ... code ...
      break;
}

The answer is no. This is because scope and visibility rules don’t allow value of the first switch to be seen inside of the nested switch block.

The following should make this clear:

int A = 7; // this is the outer_A
{   // start a new block
   int A = 3; // this is the inner_A
  // inside this block, the value of the outer_A is not visible
  // (ok, in C++ you could use the :: scope resolution operator
  //  to see the outer_A, but that is not the point)
}

In a similar way, the value of the controlling expression in the outer switch is hidden by the value of the controlling expression of the nested switch. This means the outer controlling expression has no scope within the nested switch, so it cannot jump into the middle of a nested switch.