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 abreak
after some code 2 code execution will continue at the next statement following theswitch
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 theswitch
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 theswitch
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 goto
s 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
.