The switch
statement’s cases do not require braces, period.
The purpose of braces is to create a local lexical context — that is, a space where one or more local variables may be created (and consequently given a specific, limited lifetime). In the case of a case
clause in a switch
statement, that lifetime must be limited. Consider:
switch (quux)
{
case 1:
int n = quux / 2;
break;
case 2:
std::cout << "Hello " << n << "!\n";
break;
}
If quux
is 2
, then what is the expected lifetime of n
? Is it even created? When we attempt to print the value of n
, is there even an n
to print?
The answers are no. The targeted switch-case label jumps right over the code that creates it.
In C (before C23), it is a syntactic failure as well: the target of case
labels must be a statement. But int n ...
is a variable declaration.
Sorry, I misread your language tag as C
. But I’ll leave this here since it is worthwhile info for C anyway.
We could move the declaration to before the switch
. Let’s try it:
int n;
switch (quux)
{
case 1:
n = quux / 2;
break;
case 2:
std::cout << "Hello " << n << "!\n";
break;
}
Now when quux
is 2
we jump right over the code that initializes n
with a value. With your compiler warnings turned up, you should definitely see something like:
error: 'n' may be used uninitialized
...which is UB.
These two examples are bad code. (IOW, don’t do that.)
They exist solely to demonstrate the problem.
But suppose you have a construct like this:
int n;
switch (quux)
{
case 1:
n = quux / 2;
std::cout << n << "\n";
break;
case 2:
std::cout << "Hello world!\n";
break;
}
Surely life is better, right?
Maybe. But the problems already noted still exist. You cannot declare n
local to where it is used, and in those cases where it is not used, it is uninitialized. I cannot say whether or not this is actually consequential, but it is still something the compiler must think about, so it is.
Aaaand, it is a good idea anyway to make local variables only exist when and for as little time as needed.
The solution: create a local context using {
curly braces }
. This has always been how it works in C and, consequently, C++. It has always been totally valid to do something like:
int main(void)
{
int sum = 0; // x does not exist
{ // (new local context)
int x = 10; // x exists!
while (x) sum += x--; // x exists!
} // (end of scope: x is destroyed)
printf( "%d\n", sum ); // x does not exist
return 0; // x does not exist
}
We can apply this knowledge to use in switch
statements.
switch (quux)
{
case 1:
{
int n = quux / 2;
std::cout << n << "\n";
}
break;
case 2:
std::cout << "Hello world!\n";
break;
}
This is perfectly fine and valid. The case
clauses only contain statements. n
only exists in a local context. Everything is clear and, importantly, easy for the compiler (and us humans) to reason about.
Now, return
and break
statements are attached to very specific brace-enclosed contexts. The return
is attached to a function’s brace-enclosed context. The break
is attached to a switch
(or a loop’s) context. (It is not attached to a case
!)
That means that the position of return
or break
relative to a local brace-enclosed context is irrelevant. Both are valid:
switch (quux) | switch (quux)
{ | {
case 1: | case 1:
{ | {
int n = quux / 2; | int n = quux / 2;
std::cout << n << "\n"; | std::cout << n << "\n";
break; | }
} | break;
case 2: | case 2:
std::cout << "Hello world!\n"; | std::cout << "Hello world!\n";
break; | break;
} | }
In both of these cases the local context is properly terminated before the break
jumps control to the end of the entire switch
statement.
The list of statements following a case
label are like any other list of statements: they may contain brace-enclosed local contexts in any fashion you wish.
switch (quux)
{
case 1:
{
std::cout << "inside\n";
}
std::cout << "outside\n";
{
std::cout << "inside again, lol\n";
}
break;
}
Just... try to write readable code. This leads us to the final bit:
Brace-enclosed spaces are easily elided by the compiler when they serve no purpose. So using braces for stylistic reasons is fine:
switch (quux)
{
case 1:
{
int n = quux / 2;
std::cout << n << "\n";
break;
}
case 2:
{
std::cout << "Hello world!\n";
break;
}
}
case 2:
andcase 3:
. Decide whether you actually intentcase 2:
to also execute the statements incase 3:
. If so add the[[fallthrough]];
attribute at the end ofcase 2:
. Otherwise addbreak;
. This is to make it clear to the reader that the behavior is as intended and when you enable warnings properly, then the compilers should warn about it if you don't have either. And a missingdefault:
case is usually also something that puts doubts on whether the behavior is as intended for all cases.