Hacker News new | past | comments | ask | show | jobs | submit login
C++ Exceptions: Under the Hood (2013) (monkeywritescode.blogspot.com)
115 points by arcatek on Aug 12, 2021 | hide | past | favorite | 111 comments



Working with and implementing C++ exceptions for 30 years now, including implementing exception handling for Windows, DOS extenders, and Posix (all very different), and then re-implementing them for D, I have sadly come to the conclusion that exceptions are a giant mistake.

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.


P.S. I implemented Structured Exception Handling for Win32, with help from a couple very smart people. Microsoft completely changed it for Win64, and the documentation on it is completely unhelpful to nonexistent.[1] I simply gave up on it. D on Win64 uses the exception handling mechanism I invented for the 32 bit DOS extender from the old Zortech days. It works fine, except that it cannot interact with C++ exceptions thrown by VC++ code.

[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.


Ah, Zortech C++. That indeed brings back some memories.

Walter, what do you think about Herb Sutter "new" C++ exceptions (which is basically about throwing an int/a word) ?


If there's anyone who can think of a better way, it's Herb. I'd take anything he has to say about C++ very very seriously.


On the other hand

() 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


> LibreOffice throws exceptions __a lot__

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.


> error handling and [exceptions are] not suitable for that

I disagree :).

Exceptions are fundamentally equivalent to "return sum type" error handling pattern. In an exception-enabled environment, you can imagine every function returning some Foo is really returning a {Foo, Error}. Then, every call like below, outside of try/catch block:

  value = SomeFunction();
is secretly translated to:

  maybeValue = SomeFunction();
  if(!maybeValue) { return maybeValue.error(); }
You can devise analogous translations for code in try/catch blocks.

The fundamental difference between exceptions and an Expected/Maybe mechanism is that exceptions don't force you to be explicit about all those Maybe values. If you want to handle some Error three layers up in the call stack, you don't have to litter the intermediary layers with explicit Maybes everywhere.

(This is, unfortunately, also their drawback in typical implementations - the set of possible Error types in the hidden Maybes becomes effectively open-ended, when with explicit Maybes, it's constrained and visible in source code - and, perhaps more importantly, in the ABI.)

The other day I did an experiment - I wrote two equivalent pieces of nontrivial production code, one using C++ exceptions, and other using the tl::expected library. If you looked past the syntactic noise, they mapped almost 1:1 in terms of error handling and error recovery patterns.


Obviously we can translate one Turing complete paradigm into another, but that's not very interesting.

I argue that as so very often C++ the defaults are wrong. You can easily do the wrong thing, or you can go to a lot of effort to do the right thing, and since the right thing was technically possible C++ practitioners proudly declare C++ got this correct, and I say it did not.

[See also: everything about const from West Const being endorsed in NL26 despite being silly, through to the fact that the default is mutable for no good reason; the fact char isn't necessarily signed or unsigned you need to pick one if you care; need to explicitly use a provided replacement for the array type because the default built-in array type is broken; the default meaning of the literal "Hello, world" is this awful NUL-terminated byte array using that broken default array type; Way too many dubious implicit coercions, including narrowing conversions everywhere; I could go on]

Because we're not handling truly exceptional cases we will often want to treat the OK and error cases similarly. We tried to go outside and it was raining so maybe we should get an umbrella before venturing out again, but it wasn't on fire out there so we don't need to freak out and abandon our remaining plans to flee the fire immediately.

Exceptions make this needlessly difficult whereas sum types don't. The exception deliberately changes program execution, that is in fact its purpose, whereas the sum type lets you carry around the error and its context just as you would an "OK" result, until you need it for something or you decide you didn't need it and drop it on the floor.

Bad defaults get replicated for consistency. There are a lot of bad practices -- things you definitely shouldn't do -- that are now enshrined permanently in the ABI of the C++ standard library and so for consistency you're going to inherit those practices.

As a result I agree that Expected doesn't feel nicer in C++ today than exceptions but I argue that's a language defect, in a better language you'd find Expected worked better for the unhappy paths of your program and exceptions remained available for those truly exceptional cases that the programmer did not anticipate happening. Now, one programmer might feel that even "File already exists" truly is exceptional for their scenario, while another considers "Disk I/O error" to be merely an error they can cope with and no big deal (maybe the second programmer is writing an IT forensics program). That's going to vary, but the way for a standard library to reflect that is to use Expected almost everywhere and allow the developer who thinks "File already exists" is exceptional to throw for it, not have the standard library throw everything and then you race around trying to catch what you need to and hope you didn't miss anything.


I so feel the last point, and I'd like to add, that not having [as of now] any pattern matching really hurts when working with all sorts of sum types. Coming from Rust, something like `tl::expected` and `Result` feel like night and day.


I guess people's experiences are different :-)

I find exceptions to be a perfectly fine error handling mechanism.

I certainly prefer it to cluttering my code with explicit checks for return codes and such like.


returning errors doesn't have to have cluttered code. Just because Go messed it up doesn't mean it's bad.


Don't you need exceptions though? How do you terminate arbitrary operations without exceptions?

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.


But with exceptions what do you do? You have to go modify or reimplement the source code of every intermediate function to be correct and safe when an exception is thrown at every point where it can be thrown, 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.


No you don't, you're massively exaggerating. The standard library already has at least basic if not strong exception-safety all over it. And RAII is pretty darn standard practice and guarantees basic exception safety in your own code too. You don't need strong exception safety here, just basic is sufficient for most such cases.

Go try this with std::sort (or std::adjacent_find or whatever) and tell me which of their implementations you had to modify.


Well but of course! These functions are already implemented with basic exception safety in mind. What if they weren't? This is exactly the same situation as a function that has

    callback();
which cannot be changed into

    if Err(error) = callback() {
        return error;
    }
because that would break some invariants.

Changing return type from "void" into some "result" is a mechanical change.


As I already explained: RAII is pretty darn standard practice and guarantees basic exception safety in your own code too. The music is already there and people are already dancing to it.

> What if they weren't?

Obviously the language wasn't designed for rebels. The implicit understanding with tools is that you use them the way they're meant to be used. Only in that case do you get to assume you'll reap the benefits they claim to provide. If you insist on deliberately dancing to a different tune, then you get exactly what you asked for. You can't drive against traffic and then complain people run into you.


There are interesting non-rebel cases of What if they weren't. I have a library (object only, no source) written for C (not C++) which wants callbacks. It is the only interface provided by the vendor for something that shall remain nameless. Every callback has to be wrapped in a try catch, or hell will break loose.


> Changing return type from "void" into some "result" is a mechanical change.

.. but then checking the returned val for error in every call sites is very far from mechanical change. (Attribute about unused return result can help here, with obvious drawbacks.)


I agree that a modern high-level programming language model needs an ergonomic error model. But C++ exceptions are not the only way to go. You can have error model that have similar (or even better ergonomy) than C++ while not having any of the drawbacks (like extremely complicated runtime stack, slow exception handling, messed up control flow etc.). Basically, in my personal opinion, any error handling that involves automatic stack unwinding is a failure.


Note you didn't really answer my question at all.

Right now lots of algorithms like std::search, std::find_if, etc. are not only exception-safe, but in fact exception-agnostic. Neither the algorithm, nor you, need to know a priori if your predicates will throw exceptions (which are things that may be literally impossible to know upfront), and yet despite that, (a) the algorithms will work completely correctly if any exception is thrown, (b) if you do need to do something like canceling the operations in the middle, you have a means to do that via exceptions, and (c) you will get extremely high performance as long as you don't throw an exception. That's a lot of flexibility even the most trivial implementations of many such algorithms get absolutely for free. (!) I don't know about you, but to me the fact that I can suddenly decide to "cancel" many functions halfway despite their authors never having to even think about that possibility is pure awesomeness.

So I asked "how would you do achieve {the benefits of the exception model} without exceptions" but you just said "it is possible" and... left me hanging. Well if that's really true, then how?

> You can have error model that have similar (or even better ergonomy) than C++ while not having any of the drawbacks

I don't buy it. Unless you're intentionally allowing yourself to introduce drawbacks that never existed in C++'s model. If you're really saying you can find a strictly better solution, then we're all definitely interested in hearing... and I'll believe it when I see it.

You have to realize ergonomicity (word?) isn't the only axis here. Performance is also a big one, and C++ is designed for maximizing performance in non-exceptional executions. I don't know what error models you're thinking of, but anything along the obvious stuff I've seen (like the usual "replace T with maybe<T>/optional<T>/fancy<T>") would come with far greater performance hits even in the 'happy' paths than C++ has (not to mention potential increases in memory usage, etc. in more complex cases), and even their ergonomics would be debatable depending on the situation.


> Neither the algorithm, nor you, need to know a priori if your predicates will throw exceptions (which are things that may be literally impossible to know upfront)

I don't think this property is desirable at all. I prefer to know whether a function can or cannot result in an error, ideally encoded within the type system. The C++ "everything can throw" paradigm yo describe here obfuscates the program logic and promotes bad coding practices. I know, C++ programers like to argue that "everything throws" is a natural property of any real world code, but somehow folks are able to work with Rust and Swift without too much hassle.

> If you're really saying you can find a strictly better solution, then we're all definitely interested in hearing... and I'll believe it when I see it.

A strictly better solution has been found long time: error sum types.

> Performance is also a big one, and C++ is designed for maximizing performance in non-exceptional executions. I don't know what error models you're thinking of, but anything along the obvious stuff I've seen (like the usual "replace T with maybe<T>/optional<T>/fancy<T>") would come with far greater performance hits n the 'happy' paths than C++ has (not to mention potential increases in memory usage, etc. in more complex cases)

This is again a very popular argument I've seen used by many in the C++ community, but the simple fact is that this argument is simply not true. Already very naive result type implementations using C++ show no measurable performance difference in the "good" path (with a non-trivial function), and using an optimized calling convention makes error sum types zero-cost on modern hardware.

For example, Swift uses a dedicated register to signal exceptional function result. On the "good" path, you have to zero this register in the callee and conditionally jump on its value in the caller. These operations are essentially free on any modern CPU with superscalar execution, register renaming and branch prediction. The only cost is a register and a few extra instructions which won't carry any performance impact. One can optimize this even further by using condition flags to signal exceptional result (frees up a register and saves an instruction).

To sum it up, using result types with optimized calling conventions gives you the same performance as the C++ exceptions on the good path, much better performance on the exception path, saves space (few bytes of extra instructions take much less space than the unwind information), radically simplifies the compiler (no long jumps, functions enter and exit regularly), radically simplifies cleanup (function exits regularly and can run destructors as usual), simplifies the control flow and so on.

In fact, the only disadvantage I see with this implementation is that exception propagation might be slower than a longjump if you have hundreds of nested functions. But I think you have much bigger problems if you call stack looks like that...


IMO algebraic data types pretty much solve everything that exceptions try to solve.

while also encoding into the type system that it can fail/what failure modes there are, while also forcing you to handle it locally.


Local handling is, of course, an anti-pattern in code using exceptions, and not to be encouraged. It's rare that you handle exceptions; normally, you just abort what you're doing and unwind.

If you have an API which fails often enough that you want to handle exceptions from it, it probably shouldn't use exceptions, and use some kind of conditional result or ADT equivalent instead. A concrete example would be the TryParse methods in .NET.


I think that it is much better to return an adt with "common exceptional cases" and reserve exceptions for the truly exceptional ones. For example, a lost network connection shouldn't be one, but a put of memory one makes sense.

Local handling was meant in terms of locally seeing pitential errors


What do you think of languages that use sum types for error handling, but can still unwind in a few scenarios?

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.


Now that I've been using Zig's error handling language primitives for some time, I've come to realize what the paradigm really is: a way to encode a "forwards" and a "backwards" at the same time, for the same block of code.

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.


I've read the proposals for it. It certainly looks good, yet exception handling looked good 30 years ago, too. I haven't used sum types myself, and often it takes years to discern whether things are really good ideas or not.

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.


I'm experimenting with a solution in C3 that has this behaviour. I don't have a sum type as such, but the binding acts as one. I call the binding a "failable".

    int! a = getMayError();
    // a is now either an int, or contains an error value.

    // foo(a) is only conditionally invoked.
    int! b = foo(a);

    // The above works as if it was written:
    // int! b = "if a has error" ? "the error of a" : foo("real value of a");

    // A single if-catch with return will implicitly unwrap:
    if (catch err = b) {
       /* handle errors */
       return;
    }
    /* b is unwrapped implicitly from here on and is treated as int */

    if (try int x = a) {
       /* conditionally execute if a isn't an error */
    }
I think this is kind of formalizing the poison technique but external from the call (that is, "foo" does not need to know about "failables" or return one, the call is skipped on the caller side). Here are some more examples: http://www.c3-lang.org/errorhandling/

I'd be interested in hearing what you think about this (experimental) solution Walter.


So you’ve built in monadic bind for the Either monad into the language:

  Right x >>= f = Right (f x) -- normal case
  Left y  >>= f = Left y -- error propagation case
(The slogan is “monadic bind is an overload for the semicolon”.)

I don’t expect this knowledge will dramatically change what you’re doing, but now that you know that’s how some people call it you have one more place to steal ideas from :)


No I'm quite aware of this. It's a restricted, implicit variant of it. But not also that it's not the type but the binding, which makes it slightly different from using a `Result`.


Hm. OK. I tried writing a response several times but I still feel confused. Can you explain what you mean by “not the type but the binding”? Note that I know the Haskell but not the Rust (guessing from the “Result” name) way of working in this style.

(Not necessarily relevant or correct thoughts:

- Your language still seems to mark potentially-failed values in the type system, even if it writes them T! not Either Error T or Result<Error, T>;

- The way Haskell’s do-notation [apparently implemented as a macro package in Rust] is centred around name binding seems very close to what you’re doing, although it [being monadic, not applicative] insists on sequencing everything, so fails the whole block immediately once an error value occurs;

- Of course, transparently morphing a T-or-error into a T after a check for an error either needs to be built into the language or requires a much stronger type system; Haskell circumvents this by saying that x <- ... either gives you a genuine T or returns failure immediately, which is indeed not quite what you’re doing.)


What I mean by saying "it's a binding" is that it is a property of the variable (or return channel of a function) rather than a real sum type. Consequently it does not participate in any type conversions and you cannot pass something "of type int!" because the type does not exist.

Here is an example:

    int! x = ...
    int*! y = &x;
    int**! z = &y;

    // If it had been a type then
    // int!* y = &x;
    // int!** z = &y;

    // int*! y = &x;
    // means 
    // int*! y = "if x is err" ? "error of x" 
    //                         : "the address holding the int of x"
This also means that `int!` cannot ever be a parameter, nor a type of a member inside of a struct or union.

The underlying implementation is basically that for a variable `int! x` what is actually stored is:

    // int! x;
    int x$real;
    ErrCode x$err;

    // int*! y;
    int* y$real;
    ErrCode y$err;

    // y = &x;
    if (x$err) {
      y$err = x$err;
    } else {
      y$real = &x$real;
    }

    int z;
    // y = &z;
    y$err = 0;
    y$real = &x;

The semantics resulting from this is different from if `int!` had been something like

    struct IntErr { 
      bool is_err_tag;
      union {
        int value;
        ErrCode error;
      };
    };
Which is what a Result based solution would work like. In such a solution:

    int! x ... ;
    int!* y = &x; // Ok
    int z = ...
    y = &z; // <- Type error!


You should give a serious try to sum types, btw. They're unambiguously good, and have been in use for the last 40 years at least. To me, not having them is an immediate disqualifier for a modern static language (along with some basic form of pattern matching that goes hand in hand with them).


So that sounds like the way invalid floating point operations give NaN, and then the NaN propagates everywhere. I've always found this super annoying because its often hard to figure out where the NaN comes from. Does your solution differ from this in a way that's less annoying?


The FPU does not include the source in the NaN, but that doesn't mean your own objects can't.

What I do is have the error reported at the source, and then return the poisoned object. A better way would possibly be put the error message in the poisoned object, and report the error somewhere up the call stack.


I do that a lot with the firmware I write in C.

  typedef struct
  {
    err_t error;
    int error_line;
    char *error_msg;
    ...
    ...
  } thing_t;

  // set out of range error
  thing->error = THING_ERROR_OOR;
  thing->error_line = __LINE__;
  thing->error_msg = "outofrange"
You can grep on 'outofrange' and find where the error was set.

I originally started doing that to mark 'bad' analog readings in process control equipment. I wrote my filters and control loops to be able to 'eat' occasional bad readings without barfing. Worked very well.


I often using something like this in C++

    if( nullptr != pfnErrorSink )
        pfnErrorSink( "outofrange", __FILE__, __LINE__ );
    return E_BOUNDS; // Or sometimes throw E_BOUNDS;
Where pfnErrorSink is either global, thread_local or a field keeping C function pointer provided by whoever consumes the code.


What are the return values from a poisoned object’s methods? Does a poisoned vector have a poisoned integer as its size?


> What are the return values from a poisoned object’s methods?

That's up to you. You can do it as:

1. return a poisoned value

2. return a safe value, like `0` for its size

3. treat it as a programming bug, and assert fail

4. I know `null` is hated, but it is the ultimate poisoned value. Try to call a method on it, and you are rewarded with a seg fault, which can be considered an assert fail.

5. design your poisoned object to be a real object, with working methods and all. It can be the same as the object's default initialized state.

In other words, it's necessary to think about what the poisoned state means for your use case. I use all those methods as appropriate.


How does this work with operations that return basic types like int or string? Can a string be poisoned?


Rust is one such language. I wrote a bit about exception safety in Rust here: https://users.rust-lang.org/t/c-pitfalls-hard-to-avoid-that-...

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.


We do this on all our Rust code as well (1.1 million LOC at this point).

While we can benefit from #[no_std] crates on crates.io, unfortunately we can't use any crates that require standard because the standard library does not propagate errors properly, so we maintain our own implementation of most of the standard library for Linux only, that propagates all errors using Result..

It's a huge pain point, but at least Rust allows us to do it.


> does not propagate errors properly

Is "panicking on allocation failure" the only example of this, or are there others?


Forcing the programmer to manually write stack unwinding code is not a solution.

That's like saying "garbage collecton is slow and complex, just use malloc() and free() instead".


Nobody said to manually write stack unwinding code or to only use malloc() and pair each of them with free().

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).


Those look like problems for compiler implementers (tiny subset of users) or those writing code with very tight performance requirements (large amount of C++ code does not have such reqs). In spite of the reasons given, exceptions are successfully used (in C++ too) for error handling, because they can be much nicer that shuffling error codes/result types up the stack.

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.


> they are slow when not thrown

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?


The main reason is the optimizer abandons trying to figure out flow-of-control when half the expressions can throw and present a path to the catch blocks. Furthermore, this all inhibits en-registering variables, because exception unwinding doesn't restore registers.

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.


Java JITs are considerably more advanced than this. It's expensive to put in all the additional control flow edges to model exceptions, but once done, control and data flow analyses just work, as well as loop optimizations, code motion, inlining, register allocation--all of it "just works" (TM). Then you have to spit out a metric crapton of metadata to allow searching for the correct handler at runtime, but that's the slow path.

All of that is to say that Java try blocks do not have any direct dynamic cost when exceptions are not thrown.

Even throwing exceptions and catching them locally in Java can be fast. If everything gets inlined into one compilation unit and the exception object is escape analyzed, HotSpot will absolutely just emit a jump.

Not that I disagree with your overall point--Virgil just doesn't have exceptions--but from your description here it just sounds like your compiler is far behind the state of the art in terms of optimizing exception-heavy code.


Java has a far, far more restricted view of exceptions than C++ and D have,[1] and hence more opportunities for optimization. I did implement exceptions in the Javascript compiler I implemented 20 years ago, and they're a cakewalk compared to C++. I also implemented a native Java compiler, including EH.

As for clang, see what Chandler said. But maybe things have changed in the last couple years.

[1] for example, Java doesn't have objects on the stack that need their destructors run. That's a massive simplification.


I don't know the internals of clang very well, but everything I have heard second hand (and third hand) makes me think that its approach to modeling exceptions isn't very good.


Regarding putting in all the additional control flow edges: more edges mean fewer optimizations. That's not zero cost.


I actually agree that they technically aren't zero cost (notice I didn't even write that), but the cost is indirect. I've worked on a number of Java JITs and in practice, not a lot of hot code has catch blocks, and even when so, inlining is typically so deep that lots of exception edges (e.g. arising because a possible NPE) get optimized away.

Most of the lost optimization opportunities are second-order costs, not first-order costs. Java JITs make up for the extra flow edges by focusing more on global optimizations rather than local (e.g. GVN vs LVN, global code motion, global flow-sensitive load/store elimination), etc. Generally a possible exception edge splitting a basic block doesn't hurt because the non-exceptional control flow will still benefit from flow-sensitive optimizations (i.e. it has only one predecessor anyway).

We're splitting hairs anyway. Like I said, Java JITs are significantly more advanced at optimizing exception-heavy code. I'd be really surprised if you saw anything more than a 1% increase in performance, actually, no, scratch that. I doubt you can even reliably any speedup distinguishable from noise from just disabling all support for exceptions in most Java code, unless you are talking about metadata. Top-tier JITs really are that good.


Enregistering of variables is also lost, because to restore them during unwinding they have to be retrieved from the stack.


Not sure what you mean here, but generally Java JITs generally don't use callee-saved registers at all because they need precise stack maps for GC. So whatever small amounts performance they might lose here isn't due to exceptions.


OpenJDK's (HotSpot) stack maps do support callee-saved registers -- and they are used in some special cases, like safepoint stubs (that spill all registers at a poll-point if a safepoint is triggered) -- but you're correct that they've been removed from ordinary Java calls altogether on all platforms, now.


> Not sure what you mean here

Allocating local variables into registers rather than assigning stack locations for them. Registers are faster than memory. EH unwinders restore the stack before jumping to the catch block, but not the register contents.

Stack maps wouldn't be necessary for non-pointers, like an integer variable. Stack maps also have their own performance problems, which is why D doesn't use them.


Inside a single physical frame, that contains many inlined Java methods (the current default is inlining up to depth of 15, not counting "trivial" methods that are always inlined), locals are always in registers (unless they're exhausted), and are spilled only at safepoints, e.g. when another physical call is made, which is where all languages have to spill, too. Stack maps include only pointers and incur no runtime overhead on the fast-path, and are not used when throwing exceptions, even outside the current physical frame. There's additional debug info metadata associated with compiled code, which also incurs no runtime overhead on fast paths, that maps even primitives to their registers/spill locations; that debug info also maps compiled code locations to their logical, VM bytecode, source ("de-inlining"), and is consulted in the creation of the exception when a stack trace is requested.


The term used most often for this is "spilling". I figured this is what you meant by "deregistering" but I wasn't sure, so I didn't want to assume.

> Registers are faster than memory. EH unwinders restore the stack before jumping to the catch block, but not the register contents.

I get that, which is why Java JITs don't use callee-saved registers. I mean, they use all the physical registers, of course, but their calling convention does not have callee-saved registers.


"spilling" usually means a variable is sometimes in a register, sometimes on the stack. "Enregistering" means it is full time in a register.


But what's a variable, really? After SSA renaming, optimization, SSA deconstruction, then liveness analysis, coalescing, and finally live-range splitting, variables are history and the register allocator is only dealing with live ranges, typically.


> Furthermore, this all inhibits en-registering variables, because exception unwinding doesn't restore registers.

I'm sorry, but this is just not true of at least the Itanium exception handling system used by Linux, macOS, etc on most architectures.

It does make the exception handling data quite large and throwing extra slow, of course.


At least with D a thrown exception must be a subtype of `Throwable`. I.e. you can't throw an `int`, oh, and the incredibly confusing throwing of an object that itself can throw.


> 10. has anyone yet found a legitimate use for throwing an `int`?

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.


That works until you incorporate code that throws errno.


+1 plain old return error codes and the related modern status codes are the way to go. Lots of people say this is more work. I would your comment does a good job explaining why that work is immensely useful.


> plain old return error codes and the related modern status codes are the way to go

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.


I have quit using exceptions in my own code, making everything 'nothrow'.

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?


Generic code (like you'd find in a library) is usually done with templates. Templates in D infer `nothrow`, giving them the advantage of being implicitly `nothrow` when their arguments are also nothrow. Inferring attributes this way is a major way D works.


Ah, sorry, I was thinking you were talking about not using exceptions in C++ anymore.


That's funny: I thought you were a proponent of exceptions because I read (a lot of years ago) a mail from you which said "who is going to check that printf failed"?

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..


I'd suggest to use Lisp, which is a under-rated yet powerful language.

[Why?] https://gigamonkeys.com/book/introduction-why-lisp.html

[Exceptions] https://gigamonkeys.com/book/beyond-exception-handling-condi...


"has anyone yet found a legitimate use for throwing an `int`?"

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.


Yes, but you couldn't mix that code with code that throws an `int` for other porpoises. It becomes a global straightjacket for your code.


Yes that is true. One really should not start throwing integers in code that one hopes to reuse someplace else.


> 10. has anyone yet found a legitimate use for throwing an `int`?

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...


> has anyone yet found a legitimate use for throwing an `int`?

Not sure if you consider this legitimate, but I have seen code that throws an errno.


Can you describe your ideal error handling mechanisms? Or at least other mechanisms that feel more correct?


One technique I try first is to write code that cannot fail. For example, a sort function should never fail.

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.


> Another option is to regard out-of-memory as a fatal error, not one that needs to be thrown and caught.

This is in practice almost invariably the case for large programs. Somebody (Herb Sutter maybe?) asked the major C++ Standard library implementers, and none of them really bothers to handle the tricky parts of this. If you write code to try to pre-allocate a 10TB vector of 'Z's you can probably get that to throw you the exception that you read about in the documentation, but if the library code for opening a file can't find 64 bytes for a temporary object they aren't going to bubble up an exception, they're going to crash your program and too bad.

If you write an operating system kernel, you care about running out of memory, if you write the embedded firmware for a jet engine, you care (actually you likely never allocate memory at runtime, so in that sense you don't care), but in both those cases you live in a world where many other problems are far above you out of sight, so you can afford to care about stuff like how much RAM there actually is. You don't want the C++ standard library down where you live, and they don't want your problems. Everybody who lives up above the C++ standard library doesn't care, which is why the people implementing the library don't care either.

Yes, all of Unicode processing should use U+FFFD (the replacement character). Not just UTF-8, if you have any reason to do anything Unicode related and you're in a state where other paths forward are nonsense, emit U+FFFD. Take XML. Because the people involved hated ASCII control codes XML says you can't express them in XML 1.0 (which you will in practice have to use). I don't mean they need to be escaped, I mean you intentionally cannot express them. So if you have some arbitrary ASCII text that might include control codes, you can't write that as valid XML. What to do? Emit U+FFFD whenever this problem arises. Your users go "Huh, my Vertical Tab turned into this weird character in the XML output" and you send them to talk to the XML committee which will tell them they're a sinner and must repent of the evil of Vertical Tab and now your user knows you aren't crazy and maybe they stop using XML or maybe they don't but either way your code works.


IMO this is one of the advantages of the sum-type approach: the added friction of dealing with those explicit types and values encourages you to write as much code as possible that simply can't error in the first place


I'm not going to recommend any technique I don't personally have years of experience with. Too many times a paper that makes something look great tends to have fatal flaws that only emerge years later. Sort of like WW1 strategies that sounded good but in practice produced only mud and dead bodies.

As I mentioned in another comment, I've had good success in the trenches with the poisoning technique.


I would go with the Erlang approach. Just die FFS. Let the process monitor restart you if you deserve to live.


That only makes sense if units of your code run in a loop and communicate asynchronously :). But, if you want a simple supervisor pattern in C++, then... try/catch block is your supervisor, exceptions are how your process dies. Consider:

  template<typename Fn>
  auto CallWithSupervision(Fn fn) -> decltype(fn()) {
    // supervision loop
    // configure conditions as needed
    while(true) {
      try {
        return fn();
      }
      catch(std::exception& e) {
        // log failure details
      }
      catch(...) {
        // optional: exceptions out of handled set?
        // kill supervisor.
        throw;
      }
    }
  }

  //elsewhere
  CallWithSupervision([relevant=state,&nd=captures]() { return Client(relevant, nd); });
Modify as you see fit. It's the simplest, synchronous Erlang supervisor, in C++. And it will already work with your code - exception handling is very composable this way.


Except it isn't robust.

Exceptions occur when "Very Bad No Good Undefined Horrible Things" have (at last, been found to have) happened.

So if you have no guarantee that the memory space is uncorrupted, you have no guarantee that all resources have been recovered, you have no guarantee that in attempting to deallocated resources that they have been allocated in the first place. (Read the fine fine print on exceptions in constructors and destructors.) TL;DR; Don't do that. But exceptions are what happen when people do what they shouldn't.


Who invented exceptions in the first place? Did they first appear in Java, encouraging C++ to imitate a java feature?


Software wise it seems to originate from Lisp[1].

[1]https://en.wikipedia.org/wiki/Exception_handling#History


Author here; worth noting this article was written a decade ago, and while the concepts it describes are probably still useful (or so I'm told) the text is starting to show its age. Most notably, the examples are completely broken for x86-64, as I didn't have a 64bit processor when writing this.


> When the personality function doesn't know what to do it will invoke the default exception handler, meaning that in most cases throwing from a nothrow method will end up calling std::terminate.

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.


How can that take a week to debug ? gdb would stop at the std::terminate call in your dtor, and catch throw would allow you to see exactly where the exception was thrown


Well first of all I wasn't using gdb since it's not available on the platform this code was running on.

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.


> 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.

what I mean is that, if you run that in a debugger, you're going to see something akin to this when the crash occurs (and have your ide stop exactly where the offending exception was thrown ; I don't even have to set `catch throw` for this to work): https://ibb.co/hK3skvz - at least on windows, mac, linux. What platform are you running that does not support gdb at all ? pretty much anything that isn't a PIC16F or Z80 supports it..


Implementation in Windows is quite different. Especially x86 vs x86-64 which has much less overhead. Also gets quite complicated with the Windows built-in Structured Exceptions/ asynchronous exceptions which can be translated into c++ exceptions -- and even more complexity due to different compiler options that handle these!


[2013]ish if I remember.

Also note the ABI for C++ exceptions followed by G++ et al is actually documented as part of the Itanium C++ ABI :

https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html


The doc is actually incomplete, it refers to implementation defined ABI entry points for the actual unwinding. The actual unwind tables and compensation opcodes are, IIRC, part of the DWARF standard, but last time I looked at it that standard was also incomplete and left a lot unspecified. Many of the details (including bugs that have now become part of the ABI) are basically folklore.

IIRC Ian LAnce Taylor had a series of blog posts that did shed light on a lot of these details.


There's essentially three separate moving pieces here.

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.


According to GitHub, published in February 2013, written during 2012. I feel old now.


Under the hood of GCC specifically.


That's the article I used when I implemented exceptions in an LLVM-based compiler, so it's applicable to more than just GCC.


Is your work public? If my article was useful, I'd love to have a look at what you did!


It's public, but I doubt it still compiles against recent LLVM versions! I started it 8 years ago to get a better understanding how features like classes & operator overload would work in a JS-like language. It was really fun!

https://github.com/castel/libcastel/blob/master/runtime/sour...

https://github.com/castel/libcastel/blob/master/runtime/sour...

I remember that at the time there were very few resources on personality functions, even in the LLVM doc - I had to make a lot of research before finding your articles, which were extremely helpful!

I got reminded of them yesterday after someone pinged me on a Stack Overflow answer I made at the time, asking for an updated link; after I found your long-form article I figured it would be a good topic for HN as well :)

https://stackoverflow.com/questions/16597350/what-is-an-exce...


ahhh, I see you got hit by some of the fallout of my migration from Wordpress to Blogspot. Sorry about that!

I tried to set http301s but gave up when I had to pay for it :)


Doesn't GCC support multiple exception handling options?


I'm not sure exactly what you mean but there was a switch to DWARF EH ages ago (GCC 2?)


sjlj is still supported, at least on some platforms, e.g. MinGW.


I'm not sure what your point is - mine was that the original post is specifically about GCC, not Standard C++.


“Standard C++” just specifies `throw` and `catch` (plus what happens when you traverse a block, nothrow declaration etc)

Every implementation has to do something to actually be standard C, and this is an example. It’s rather similar on other hardware, but as far as the standard goes that is irrelevant.


Mainly that exception handling doesn't have a single solution, even in GCC. I'm agreeing with you that this article only highlights one version of exception handling for one compiler.

There's also potential operating system involvement that's not really covered in this article.


Very interesting read, however it is under the hood on a specific implementation.


Just dropping by to say hi to a fellow c++ing monkey


:D




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: