LibreOffice now has Clang -Wimplicit-fallthrough
enabled to catch places where a break
has been missing between cases in a switch
statement. See “Re: clang -Wimplicit-fallthrough and missing breaks” for some further details.
Of course, a code base as large as LibreOffice has quite a number of places where such fall-through from one case to the next is intentional, and where warnings thus need to be suppressed. The solution is the [[clang::fallthrough]]
annotation (which will be available as plain [[fallthrough]]
in C++17), wrapped up as a SAL_FALLTHROUGH
macro for now (so it doesn’t confuse other compilers).
All fine and dandy.
Until you realise that there is quite a number of places like the following in the code base:
Foo * p; switch (kind) { default: assert(false); // cannot happen case Kind1: p = new Foo(1); break; case Kind2: p = new Foo(2); break; case Kind3: p = new Foo(3); break; }; p->foo();
Maybe kind
is of an enumeration type that can only legitimately take on one of the values Kind1
–Kind3
(see “”enum union” with set of values restricted to exactly its enumerators?” for a discussion how such a type cannot reasonably be modeled in C++), or it is a type that can take on a wider set of values, but some invariant guarantee is that it will only take on values Kind1
–Kind3
here. Regardless, some compilers will warn about unhandled cases, and/or about p
being used uninitialized in some cases. So the default assert
part got added at some point in time, logcially marking any unhandled cases as unreachable. If a compiler/NDEBUG
-configuration combo is smart enough to conclude that the assert
is non-returning, it will not emit a warning. If, on the other hand, the compiler/NDEBUG
-configuration combo assumes fall-through to case Kind1
, it will not emit a warning about p
being used uninitialized, either.
All fine and dandy.
Except that, in the NDEBUG
case, Clang will now warn about a missing [[fallthrough]]
annotation between the default branch and case Kind1
. Under non-NDEBUG
configuration (i.e., assert
actually expands to some code), it is smart enough to conclude that the assert(false)
will never return, so it concludes that there will be no fall-through, so will not warn. But under NDEBUG
configuration (i.e., the assert
expands to nothing), it sees an empty statement in the default branch, causing fall-through, causing a -Wimplicit-fallthrough
warning.
But when you add a [[fallthrough]]
annotation to appease the NDEBUG
configuration, the non-NDEBUG
configuration will start to complain about an unreachable [[fallthrough]]
annotation (as it considers the assert
to be non-returning, see above).
Caught between a rock and a hard place.
One way out is to rewrite the code as follows:
Foo * p; switch (kind) { case Kind1: p = new Foo(1); break; case Kind2: p = new Foo(2); break; case Kind3: p = new Foo(3); break; default: for (;;) std::abort(); // cannot happen }; p->foo();
The std::abort
marks the default branch as dead independent of NDEBUG
. The infinite loop around it is there to make even the most lame compiler aware that that statement will not return, so it will refrain from emitting a warning about p
being used uninitialized. (And the default branch can now go at the end, where it feels more naturally at home.)
And no, there should be no fears that the second variant will behave differently now in production NDEBUG
builds. In the first variant, should kind
ever take on an invalid value, it would silently treat it as Kind1
, proceeding (though, of course, proceeding in the most unfortunate of “defensive programming” ways), not (immediately) crashing in the user’s face. In the second variant, it would abort outright (“crashing in the user’s face!”, one might fear). But that’s a red herring. The assert
(and, similarly, the std::abort
) codifies the invariant that kind
must be one of Kind1
–Kind3
here. So if it ever did have a different value, all bets about the state of the program would be off, anyway. And it would better fail fast.
