1. they are very hard to understand all the way down
2. they are largely undocumented in how they're implemented
3. they are slow when thrown
4. they are slow when not thrown
5. it is hard to write exception-safe code
6. very few understand how to write exception-safe code
7. there is no such thing as zero-cost exception handling
8. optimizers just give up trying to do flow analysis in try-exception blocks
9. consider double-fault exceptions - there's something that shows how rotten it is
10. has anyone yet found a legitimate use for throwing an `int`?
I have quit using exceptions in my own code, making everything 'nothrow'. I regret propagating exception handling into D. Constructors that may throw are an abomination. Destructors that throw are even worse.
[1] I attended a presentation on it by MS soon after Win64 came out. All I could think of at the end was "what's a cubit". I understood exactly nothing about it.
Walter, what do you think about Herb Sutter "new" C++ exceptions (which is basically about throwing an int/a word) ?
() they solve a handful of use-cases really really well.
() if you are writing relatively decent C++, most code is pretty much exception safe already
() lots of abstractions are dangerous when mis-used
() they are sufficiently low cost that they almost never show up in the fairly extensive perf profiling I do on a large real-world application (LibreOffice). And LibreOffice throws exceptions __a lot__
(*) Except for toolchain writers, nobody cares how they are implemented
They're terrible for this. C++ Exceptions are a perfectly good exception mechanism, but what C++ programmers are trained to do with them isn't exceptions but error handling and they're not suitable for that.
"I tried to create the file but it already existed" is an error - and now you're going to write the unhappy path code, this is the wrong place to have exceptions.
"I tried to create the file but the OS now says the abstract concept of files is alien to it" is an exception. You are not prepared for this eventuality, the best you can do is explain to the user as best possible what happened and hope a human knows what to do.
Like say you call an algorithm (like std::sort) and during a callback (e.g. in the comparator) you decide to cancel the operation (perhaps user-requested). With exceptions it's easy; you just throw an exception and then catch it. No need to touch or even know the intermediate callers. But without exceptions what do you do? You have to go modify or reimplement the source code of every intermediate function, which is a giant waste of effort at best, and in reality a likely vector for introducing code duplication, brittleness, and bugs.
The point is, retrofitting exceptions onto existing codebase is a lot of pain.
Interruptible functions have the API they have because they have been designed with exceptions in mind for interruptions. If there were no exceptions, the callbacks would have had a different API. A special return value could be used to signal an interruption.
Reasonnable compromise, or should we get rid of all unwinding always? And if so, do we abort() or do we ask users to handle any and all possible errors.
The usual way control flow progresses is forwards, but when an error occurs, it goes backwards, over the defers and errdefers.
C++ exception handling and other languages with destructors force you to do this declaratively, but then don't give enough control over exactly the situations they matter in: setup and teardown.
Meanwhile with explicit control, you just encode exactly what happens in the "backwards" control flow. No surprises, no trying to figure out what happens based on declarative rules. Once I figured this out, I was able to use it to simplify the logic of some things in the self-hosted compiler that are extremely error prone in the C++ implementation:
* Lazy source locations: passing in "none" for a source location, and then handling the "error.SourceLocationNeeded" and then doing the expensive calculations to find the source locations before retrying the operation.
* Generic instantiations: returning "error.GenericPoison" for when a type parameter cannot be determined without information from the callsite. In this case, the analysis is cleanly aborted and function marked as generic.
I'm pleased with how this turned out, and I've started to think of other languages in terms of how they map to this "forwards" and "backwards" control flow concept.
What I personally use is the "poisoning" technique. This involves marking an object as being in an error state, much like a floating point value can be in a NaN state. Any operation on a poisoned object produces another poisoned object, until eventually this is dealt with at some point in the program.
I've had satisfactory success with this technique. It does have a lot of parallels with the sum type method.
In short, while the problem is mitigated somewhat compared to C++, it's still one of the most common causes of bugs in unsafe Rust code.
Rust programs can choose to abort on all panics, rather than unwind. Firefox does this, for example.
That's like saying "garbage collecton is slow and complex, just use malloc() and free() instead".
There are other very good solutions that involve explicit structure. For example
- do not free things at all, just reserve a big chunk of address space and let the OS populate it as needed. When the process quits the OS frees everything automatically.
- do the same thing for parts of the program but implement the "OS part" in the program itself. There are variations of this known by terms such as "memory arena" or "pools". Basically, just take care to group allocations by end of lifetime. Then you can free everything in one go without tracking each lifetime individually in a stack frame (which is insane).
Really, as an end-user the issue with exceptions in C++ is another:
a) it's impossible to figure out what throws by looking at code.
b) it's (nearly) impossible to ensure that something doesn't throw
This means on one hand that one has to assume that any code can throw and manage resources appropriately, which is by now known and there are well-established idioms around it. On the other hand though it also means that the silliest error from a tiny library can bubble up into the event loop/main function and terminate an application.
Swift's syntax for exceptions illustrates what I mean, even though Swift does not unwind the stack.
I thought the not-thrown case was pretty much zero-cost, at least in newer versions of Clang... How much of a slow down are we talking here?
If you want your code to be fast, use 'nothrow' everywhere.
I don't know about newer versions of Clang, but I recall Chandler Carruth mentioning that LLVM abandons much optimization across EH blocks as infeasible.
I use that a lot in constexpr computations -- to stop the compilation, I usually do 'throw __LINE__'.
-- Using a more complex type is not warranted -- there is no catching end in constexpr.
-- And in case the same routine ends up called non-constexpr, it will be easy to identify the place that called 'throw' -- line numbers are unique without additional effort. Just don't put two throws on the same line.
Why yes I just love to get a "Error one of the billion files this application tried to load wasn't available ErrorCode: ERR_MISSING_FILE_FUCK_WHO_KNOWS_WHICH". What I like about exceptions is that they make information that can't be encoded in a 32 bit integer value available to top level error handlers.
Assuming not all code you use is your own, how does this work in combination with other code (like the STL) which is not nothrow?
And this remain true: a lot of things can fail (arithmetic operations, every IO, etc) so the error system must be very "lean" otherwise the "happy path" is drowned in the error propagatio/handling code..
[Why?] https://gigamonkeys.com/book/introduction-why-lisp.html
[Exceptions] https://gigamonkeys.com/book/beyond-exception-handling-condi...
I could image throwing an int when writing a shell utility and throwing the return value of main as an int but I suppose doing that usefully would be pretty rare. Usually one cares more about whether a shell utility is successful or not not so much about the precise reason it failed. So, while I could imagine doing that, I don't see myself going for that option too likely.
When an integer is the only value I need in the catch handler, I sometimes throw them, but only negative integers.
https://docs.microsoft.com/en-us/openspecs/windows_protocols...
Not sure if you consider this legitimate, but I have seen code that throws an errno.
Consider the case of running out of memory. One option is to pre-allocate all the memory the algorithm will need, then it can't run out of memory. Another option is to regard out-of-memory as a fatal error, not one that needs to be thrown and caught.
Another example is UTF-8 processing. Early on, I did the obvious when invalid UTF-8 sequences were discovered - throw an exception. But this got in the way of high speed string processing (exceptions, even in the happy path, are slow). But what does one do anyway with such input? abort the display of the text? Nope. The bad sequence gets replaced with the Unicode "replacement character". This turns out to be common practice, and now my UTF-8 processing code cannot fail! And it's smaller and faster, too.
It's a fun challenge to figure out how to organize the program so it can't fail.
As I mentioned in another comment, I've had good success in the trenches with the poisoning technique.
This is an interesting tidbit that cost me a week of debugging recently - a try/catch block at the top of the call stack wasn't catching an exception.
We set up an exception handler that calls main in a try/catch block, so that any thrown exceptions can be caught, processed, and dispatched to our crash-logging system.
But destructors are marked nothrow by default. So we had a case where an object was destroyed, and about 10 levels down from its destructor some other system threw an exception, intending it to be caught by the top-level catch block.
But during stack unwinding we passed through the nothrow destructor and std::terminate got called before unwinding got to the top-level try/catch.
Second, the std::terminate call doesn't get called from the dtor, it gets called from the stdc runtime in the call frame of the throw (with OS code in between). The stack isn't actually unwound at this point, it's more like the stdc runtime is walking up the call stack looking for a landing pad at each frame.
Third, I didn't know about how this all worked, so I was trying to piece it all together for the first time.
Yes, I saw the throw happen. But the symptom was then that the program just... terminated.
Also note the ABI for C++ exceptions followed by G++ et al is actually documented as part of the Itanium C++ ABI :
IIRC Ian LAnce Taylor had a series of blog posts that did shed light on a lot of these details.
At the bottom layer you have the unwind API. This is actually generally defined by the platform-specific ABI, although the definition here on most Unixes is "we have a .eh_frame section that's basically the same as .debug_frame in DWARF." The API is provided by some platform library (libunwind), and it's language-agnostic.
Part of the generic unwind structure is that each stack frame has a personality function and a Language-Specific Data Area, and the unwind semantics will call the personality function to figure out whether to drop off into the stack frame, and where in the function to drop off, or continue unwinding the stack. The personality function itself uses the LSDA to figure out what to do. The personality function lives in the language standard library (e.g., libstdc++), and the LSDA format is of course entirely undocumented (i.e., read Ian Lance Taylor's blog posts as the best documentation that exists).
The final level of the ABI is how you actually represent the exceptions themselves. This is provided by the Itanium ABI and describes the names of the functions needed to effect exception handling (e.g., __cxa_begin_catch), the structure layouts involved, and even some nominal data on how to handle foreign exceptions which don't really exist.
And that's not entirely true for all systems. ARM notably uses a different ABI exception handling, which is rather close to the standard Itanium exception handling except the details are different. Some operating systems choose to forgo unwinding-based exceptions in lieu of setjmp/longjmp as the standard ABI, which of course requires different versions of everything. And Windows has an entirely different form of exception handling which isn't centered around unwinding but actually calling exception handlers as new functions on the stack, requiring frame links to the original-would-be-unwound-to function.