Exceptions are a topic near and dear to my heart, mostly because I have some strong opinions about the benefits and disadvantages of exceptions. But this isn’t a blog posting about whether exceptions are good or not. Instead, this is a post about using exceptions when designing a C or C++ framework.
There’s really two different cases to discuss on this topic. One is designing a C framework, and the other is designing a C++ framework.
If you are designing a framework with a straight C interface, you can’t use exceptions at all, for any reason. That’s because the C language doesn’t define exceptions (ignoring floating-point exceptions, which are a slightly different beast). So even if your implementation is in C++ and the C headers are merely a wrapper, you cannot throw exceptions across library boundaries. You can’t guarantee that the caller of your library will be able to do anything useful with the exception because the consumer could be written in any language able to work with C ABIs, like a C application, or a VisualBasic application, etc. In fact, the language consuming your framework may not even have the concept of exceptions to begin with! So when making a C framework, there’s no valid situation where you can throw an exception.
If you are designing a C++ framework, the waters get a bit murky. The C++ specification makes no assurances about the ABI for exceptions. This means that one C++ compiler could define the layout and handling of exceptions differently from another C++ compiler. However, some vendors have tried to alleviate this issue by creating a stable ABI for their exceptions. Unfortunately, the road to hell is paved with good intentions. That stability is a false safety net unless there is only one compiler and one ABI for that particular platform. For instance, if you use Visual Studio to develop your framework, nothing says the consumer won’t be using gcc over MinGW. Or if you use XCode (Clang or gcc) on the Mac, the consumer may be using CodeWarrior.
Taking a more concrete example of differences: on Windows, there are (at least) three different common exception specifications you can run into. The VC++ exception implementation (used for try/catch blocks in C++), the gcc exception implementation (used for try/catch blocks in C++), and the structured exception handling implementation (used for SEH exceptions, __try/__catch). All three of these use entirely different mechanisms to provide roughly equivalent functionality. The way the stack is unwound in all three situations are drastically different, as are the runtime memory layouts of the exception objects themselves. If one mechanism throws an exception, either of the other mechanisms will certainly be unable to do anything useful.
So you cannot rely on exceptions being caught across library boundaries even for C++.
Well, I lied a little bit. You can rely on exceptions in C++ if you’re willing to sacrifice code portability. Some machines have a well-defined exception ABI as a convention, such as Itaniums. Assuming that all compilers involved match that convention, then you can safely cross library boundaries. However, the barrier is still quite large in that your code is now tied to that particular platform simply to handle edge case errors!
If your C++ code uses exceptions internally, the one reasonable approach you can take is to flatten your exceptions into error codes using a try/catch block within your public APIs. For instance:
class exception_base : public std::runtime_error { public: exception_base( const char *message, int code ) : std::runtime_error( message ), Code( code ) {} const int Code; }; class MyBadThingException : public exception_base { public: explicit MyBadThingException( const char *message ) : exception_base( message, 42 ) {} }; // Public API exposed in the header file int DoSomethingInteresting( OpaquePtr ptr ) { try { static_cast< MyClass * >( ptr )->DoSomethingInteresting(); } catch (const exception_base &ex) { return ex.Code; } catch (...) { return kCatchAllErrorCode; } return 0; }
You may dislike co-opting the return value to pass status information, but you can see how this would be modified to return status as a by reference parameter. The important part is that you’re catching exceptions at library boundaries and converting them into a more framework-friendly mechanism.
If you’re interested in how exceptions are implemented under the hood, the following links should give you plenty of details to consider:
A Crash Course on the Depths of Win32 Structured Exception Handling
Exception Frames
Itanium C++ ABI: Exception Handling
Reversing Microsoft Visual C++ Part I: Exception Handling
tl;dr: don’t throw exceptions across library boundaries ever.