C++ Force compile-time error/warning on implicit fall-through in switch
C++Switch StatementCompiler WarningsC++ Problem Overview
switch
statements can be super useful, but lead to a common bug where a programmer forgot a break statement:
switch(val) {
case 0:
foo();
break;
case 1:
bar();
// oops
case 2:
baz();
break;
default:
roomba();
}
You won't get a warning obviously since sometimes fall-through is explicitly desired. Good coding style suggests to comment when your fall-through is deliberate, but sometimes that is insufficient.
I'm pretty sure the answer to this question is no, but: is there any way currently (or proposed in the future) to be able to ask the compiler to throw an error (or at least a warning!) if your case
does not have at least one of break;
or something to the effect of // fallthru
? It would be nice to have a defensive programming option for using switch
statements.
C++ Solutions
Solution 1 - C++
Well clang has -Wimplicit-fallthrough
which I did not know about but found by using -Weverything
. So for this code it gives me the following warning (see it live):
warning: unannotated fall-through between switch labels [-Wimplicit-fallthrough]
case 2:
^
note: insert '[[clang::fallthrough]];' to silence this warning
case 2:
^
[[clang::fallthrough]];
note: insert 'break;' to avoid fall-through
case 2:
^
break;
The only documentation I can find for this flag is in the Attribute Reference which says:
> The clang::fallthrough attribute is used along with the > -Wimplicit-fallthrough argument to annotate intentional fall-through between switch labels. It can only be applied to a null statement > placed at a point of execution between any statement and the next > switch label. It is common to mark these places with a specific > comment, but this attribute is meant to replace comments with a more > strict annotation, which can be checked by the compiler.
and provides an example of how to mark explicit fall-through:
case 44: // warning: unannotated fall-through
g();
[[clang::fallthrough]];
case 55: // no warning
This use of an attribute to mark explicit fall-through has the disadvantage of not being portable. Visual Studio
generate an error and gcc
generates the following warning:
warning: attributes at the beginning of statement are ignored [-Wattributes]
which is a problem if you want to use -Werror
.
I tried this with gcc 4.9
and it looks like gcc
does not support this warning:
>error: unrecognized command line option '-Wimplicit-fallthrough'
As of GCC 7, -Wimplicit-fallthrough
is supported and __attribute__((fallthrough))
can be used to suppress the warnings when fallthrough is intentional. GCC does recognize "fallthrough" comments in certain scenarios, but it can be confused fairly easily.
I do not see a way of generating such a warning for Visual Studio
.
Note, Chandler Carruth explains that -Weverything
is not for production use:
> This is an insane group that literally enables every warning in Clang. > Don't use this on your code. It is intended strictly for Clang > developers or for exploring what warnings exist.
but it is useful for figuring out what warnings exist.
###C++17 changes###
In C++17 we get the attribute [[fallthrough]] covered in [dcl.attr.fallthrough]p1:
> The attribute-token fallthrough may be applied to a null statement (9.2); such a statement is a fallthrough statement. The attribute-token fallthrough shall appear at most once in each attribute-list and no attributeargument- clause shall be present. A fallthrough statement may only appear within an enclosing switch statement (9.4.2). The next statement that would be executed after a fallthrough statement shall be a labeled statement whose label is a case label or default label for the same switch statement. The program is >ill-formed if there is no such statement. > >... > > > > [ Example: void f(int n) { void g(), h(), i(); switch (n) { case 1: case 2: g(); [[fallthrough]]; case 3: // warning on fallthrough discouraged h(); case 4: // implementation may warn on fallthrough i(); [[fallthrough]]; // ill-formed } } —end example ]
Solution 2 - C++
I always write a break;
before each case
, as follows:
switch(val) {
break; case 0:
foo();
break; case 1:
bar();
break; case 2:
baz();
break; default:
roomba();
}
This way, it is much more obvious to the eye if a break;
is missing. The initial break;
is redundant I suppose, but it helps to be consistent.
This is a conventional switch
statement, I've simply used whitespace in a different way, removing the newline that is normally after a break;
and before the next case
.
Solution 3 - C++
Advice: if you consistently put a blank line in between case clauses, the absence of a 'break' becomes more visible to a human skimming the code:
switch (val) {
case 0:
foo();
break;
case 1:
bar();
case 2:
baz();
break;
default:
roomba();
}
This isn't as effective when there's a lot of code inside individual case clauses, but that tends to be a bad code smell in itself.
Solution 4 - C++
Here's an answer to compulsively hate.
First, switch
statements are fancy gotos. They can be combined with other control flow (famously, Duff's Device), but the obvious analogy here is a goto or two. Here's a useless example:
switch (var) {
CASE1: case 1:
if (foo) goto END; //same as break
goto CASE2; //same as fallthrough
CASE2: case 2:
break;
CASE3: case 3:
goto CASE2; //fall *up*
CASE4: case 4:
return; //no break, but also no fallthrough!
DEFAULT: default:
continue; //similar, if you're in a loop
}
END:
Do I recommend this? No. In fact, if you're considering this just to annotate a fall-through, then your problem is actually something else.
This sort of code does make it very clear that a fall-through can happen in case 1, but as the other bits show, this is a very powerful technique in general that also lends itself to abuse. Be careful if you use it.
Forgetting a break
? Well, then you'll also occasionally forget whatever annotation you pick. Forgetting to account for fall-through when changing a switch statement? You're a bad programmer. When modifying switch statements(or really any code), you need to first understand them.
Honestly, I very seldom make this kind of error (forgetting a break)--certainly less than I made other "common" programming errors (strict aliasing, for example). To be safe, I currently do (and recommend you to do) just write //fallthrough
, since this at least clarifies intention.
Other than that, it's a just a reality that programmers need to accept. Proofread your code after you write it and find the occasional problem with debugging. That's life.
Solution 5 - C++
You can use a "python style" switch as follows (see it live, perhaps to one's surprise this is actually legal construct accoriding to the standard).
The key trick is to make the entire switch statement a simple statement - i.e. there is scope block, as usual.
Instead, we start a chaining if-statement interspersed with case
labels.
The if statement itself is unreachable, but you can chain with else if(/*irrelevant*/)
to continue the switch in a way that never falls through.
The else
nicely excludes the other branches. In effect it's a "switch tree"
now, with labels that bypass the conditions.
switch (a) if (false)
case 4:
case 5:
case 6:
std::cout << "4,5,6" << std::endl; //single statement
else if (false)
case 7:
case 8:
{ //compound is ok too
std::cout << "7,";
std::cout << "8" << std::endl;
}
else if (false)
default:
std::cout << "default" << std::endl;
Note that:
-
the actual condition expression is unused (the case labels jump past the condition)
-
you could actually use a macro like
#define FORCEBREAK else if(false)
to highlight the meaning.
Cons:
- That last aspect kind of highlights a flaw: it's strictly more work, and C++17
[[fallthrough]]
is mostly just better - You now need to silence the unreachable code warning that compilers give
(e.g. GCC requires
-Wno-switch-unreachable
to accept it without warning)
Pros:
- It's fancy
- It teaches about the flexibilities of
switch
statements - It does force people to correctly break their switch cases, or it will fail with compile-time errors
- It applies to C as well with minor modifications
#include <iostream>
void switchless(int a) {
switch (a) if (false)
case 4:
case 5:
case 6:
std::cout << "4,5,6" << std::endl; //single statement
else if (false)
case 7:
case 8:
{ //compound is ok too
std::cout << "7,";
std::cout << "8" << std::endl;
}
else if (false)
default:
std::cout << "default" << std::endl;
}
int main() {
for (int i = 0; i < 10; ++i)
switchless(i);
}
Prints
default
default
default
default
4,5,6
4,5,6
4,5,6
7,8
7,8
default