I recently came across the article Stop using -fno-exceptions by “LH_Mouse” on Reddit. I initially expected some strong counter-examples to anti-exception propaganda, e.g. the modern performance of exceptions.
The article instead explains some high-level concepts about exception safety and finishes with a “why -fno-exceptions
” ? This article is my answer to that question without bandying about aging performance complaints.
Note: this article is a little light on examples for my tastes and is certainly a weaker argument for it. Don’t treat this article as a source of truth but take it as more of an opinion piece.
Impossible strong exception safety guarantee
If this operation throws an exception, there is no effect.
The strong guarantee is impossible to provide for some types of containers and data structures, or impossible to provide without severe algorithmic complexity and inefficiency.
flat_set
for instance is a very simple container that cannot be left in a valid state on exception, except by dropping all of its contents. If an exception is thrown while shuffling values around, the container must either be left with a hole (breaking the ordering invariant) or left with a duplicate (breaking the unique-keys invariant).
Let’s not even get started on what led to C++17’s variant
and its valueless_by_exception
state.
Of course, if some code has these sorts of types, it’s not a big deal if it’s compiled with -fno-exceptions
.
Basic exception safety guarantee and data loss
If this operation throws an exception, all invariants are preserved.
Looking again at the example of flat_set
, the only way to provide any exception safety is with the basic exception guarantee and simply dropping the container’s contents. This means that all of the data is lost.
At least invariants are preserved, right?
If this happens and the user then saves their document/game/whatever, they just lost their data.
If the code can’t provide the strong exception guarantee on everything, the code shouldn’t allow exceptions to be thrown. -fno-exceptions
to the rescue.
Foreign function interfaces at any level
Never throw exceptions from foreign (‘foreign’ as in ‘foreign languages’) callbacks.
Code authors shouldn’t need to know if their code is called in such a context, because otherwise that means their code isn’t composable.
Remember, the proposed rule means that code can never ever allocate memory if it’s ever in a context that might be a foreign callback.
How does the author of foo
know that it’s called by bar
which is invoked by baz
that gets called from a C callback? The author doesn’t, and they shouldn’t have to.
If the author of the foreign callback is just expecting to catch all exceptions, how do they know which exceptions will be thrown? All they can do is catch(...)
and pray there’s something meaningfully correct to do in that case (hopefully some kind of general “everything failed” callback result).
-fno-exceptions
to the rescue.
My take on C++ exceptions
Most codebases aren’t fully exception-safe. It’s incredibly difficult to make code truly exception safe, even in C++11 and later. Exceptions are thrown from far more places than a developer might think they are.
Of course, the problem exception is mostly std::bad_alloc
, and that is only that big of a problem because C++ allows move constructors to allocate. Were we to have a -fno-alloc-exceptions
(most software can’t deal with bad_alloc
sensibly anyway) and a -fforce-noexcept-move
we might be just fine without -fno-exceptions
, but that is sadly not the C++ we have.
The surprise violation of code flow is the other big problem. The C++ user does not need to give permission to C++ to do this. A few languages like Swift have improved this area by requiring the caller of an throwing function to acknowledge at the call site that an exception might be thrown. This makes it a lot easier to avoid being surprised when function implicitly exits right in the middle of a sensitive data mutation. Unfortunately, again due to the aforementioned bad_alloc
and throwing moves, it’s incredibly difficult to write almost any interesting line of code in C++ that doesn’t have the potential to throw.
A counter argument might be to just toss a try
/catch(...)
block mutations that temporarily break invariants. The problem is that all this does is give the code a chance to provide a strong or basic exception safety guarantee (which again, may be impossible or undesirable). We won’t even know for sure if this can get triggered until runtime. What we really want is a way to get a compile-time error is a potentially-throwing operations happens in a sensitive area of code. C++ lacks such a facility.
There’s various alternative or additional change that could be made to make exceptions safe, reliable, and far more useful. Unfortunately, any significant fix here can never be done in C++ because of backwards compatibility concerns. C++ is forever stuck with dangerous exception semantics. C++ (also C) simply isn’t a language that took safety or reliability or even general error handling/prevention as a core tenet of its underlying early design.
An example is std::optional
. It’s easy to misuse: just call optional::value()
without remembering to check whether the optional
is engaged first. The result? A new exception type that the existing codebase isn’t trying to catch and the new optional
-using code probably wasn’t expecting to throw. A coding error that should be a compile-time failure is instead a runtime error that probably just crashes the process. Yay.
I’d argue that there’s not a ton of cases where exceptions are an improvement over the many other error-propagation mechanisms available to C++. Between [[nodiscard]]
, monadic types and rsults, better invariants, and so on, developers are in good hands without exceptions in the vast majority of cases.
C++ sadly doesn’t offer some of the language support like pattern matched destructuring or semantic macros necessary to get some of the best modern error handling primitives, but C++ can reasonably emulate much of it with fairly error-resistant interfaces. Exceptions are primarily useful when code needs to escape from a deep call chain (e.g. under heavy recursion), and even then I’ve typically found a little refactoring solves the problem better anyway.
C++ exceptions are dangerous. -fno-exceptions
. Always.