Hacker News new | past | comments | ask | show | jobs | submit login

I recently reimplemented machinery in our compiler which implements C++ exceptions. I figure it might be useful to share a few interesting issues.

The way C++ exceptions are used creates very interesting interactions in the design of their implementation.

There a hidden cost many forget: call frame information restricts what the compiler can do. Why is this the case? Even if exceptions are super rare, the compiler still needs to make sure the program will obey the language rules if an exception is somehow thrown at an appropriate program point.

The compiler must emit instructions which can be represented by the call frame information in order for the unwind to be able to reason about the stack.

This is usually not a problem on Linux because the DWARF CFI is very expressive. That expressiveness comes at a cost: it is quite inefficient when it comes to size.

Other platforms, Windows NT (ARM, x64 and IPF) and iOS, recognized that this increase in size is a bad state of affairs and thus aimed to make the CFI more compact. By doing this, they greatly reduced the size of CFI but unfortunately created restrictions on what a compiler can do.

As for trickiness inherent in C++ exceptions, C++ supports a fairly esoteric feature: exceptions may be rethrown without being in a try or catch:

  void rethrow() {
    throw;
  }
An easy way to make this sort of thing work would be to thread a secret in/out parameter which represents the exception state.

But how is this typically implemented?

Well, remember, the ethos of exceptions in C++ is that they are rare. Rare enough that implementors are discouraged from optimizing the speed of C++ exceptions.

Instead, thread local storage is typically used to go from any particular thread back to it's context.

Things get pretty darn complicated pretty quickly with features like dynamic exception specifications:

  void callee() throw(double) {
    throw 0;
  }
  void caller() {
    try {
      callee();
    } catch (...) {
      puts("got here!");
    }
  }
On first examination, "got here!" should be unreachable because the call to "callee" results in a violation of the exception specification.

However, this is not necessarily the case! What _actually_ happens is that some code runs between the throw and the catch: std::unexpected is called.

Now, std::unexpected might throw an exception of it's own! If this new exception matches the exception specification, the exception would pass into the catch block in "caller". If it doesn't, the exception thrown within std::unexpected might result in another violation!

Wow, this is get complicated... OK, so what happens if it results in another violation? Well, the exception gets cleaned up and replaced with, you guessed it, another exception! We'd be left with an exception of type std::bad_exception leaving std::unexpected and thus "callee". Because the catch clause in "caller" is compatible with std::bad_exception, control is transferred to the catch block.

This is the tip of the iceberg. A huge amount of machinery is sitting around, waiting to engage to make exceptions work.




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

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

Search: