In modern C++, exceptions are the preferred method of runtime error reporting and handling. Yes, there are some cases where other forms of error reporting may be more appropriate—error codes, for example—but generally exceptions are preferred. When building a brand new library or application using C++, it’s best to write exception-safe code and use exceptions consistently for error reporting.
Even so, there are many cases where it is simply not possible to use exceptions or where exceptions must not be used. There’s an awful lot of legacy C++ code that does not use exceptions, and worse, is not exception-safe. Often, it is desirable to start using newer libraries in these legacy codebases, to help improve code quality, reduce complexity, and make the code more easily maintainable.
Note that when I say “newer libraries,” I don’t necessarily mean anything fancy. As a mundane but extremely common example, we might consider the case where we decide to start using Standard Library containers as replacements for hand-rolled data structures. The Standard Library containers rely on exceptions for reporting certain runtime errors like out-of-memory errors, so one must be careful when introducing them into a non-exception-safe codebase.
[Aside: For a terrific overview of exception-safety and best practices for writing exception-safe code, I highly recommend Jon Kalb’s “Exception-Safe Coding in C++” talk.]
So, how can we safely introduce use of exception-throwing libraries into a codebase that is not exception-safe? The most straightforward approach is to start by carving out small pieces of the codebase that can be made exception-safe, and encapsulate exception usage within those pieces. For example, you might start with an individual class or component, make it exception-safe, then start using exceptions for error handling within that class.
When doing this, a natural exception boundary is formed: exceptions are used for error handling on one side of the boundary, but they must not be permitted to leak across the boundary. Using our example of a single exception-safe class: exceptions can be used internally by the class, but every public member function must be sure to catch all exceptions and turn them into errors that can be used by external callers.
Note that this idea of an exception boundary is not limited to legacy code. There are many other circumstances in which an exception boundary is required. Consider the case where C++ is used to implement a reusable shared library (DLL), and the library has either a C interface or a COM interface. In either case, you must not let exceptions cross the API boundary. Thus, the API boundary is also an exception boundary: you can use exceptions however you like within the implementation of the library, but you must ensure that you catch them all at the API boundary and either transform them into error codes or otherwise handle them appropriately.
A Simple Exception Boundary
Let’s consider a very simple example of a boundary function that uses exception-throwing code in its implementation, but that cannot leak any exceptions to its callers. For our example here, we’ll consider a C function that returns an HRESULT:
extern "C" HRESULT boundary_function() { // ... code that may throw ... return S_OK; }
The actual code that may throw is irrelevant: it is whatever code is required to implement this function. The only important thing is that the code that may throw might throw an exception. From a correctness standpoint, we should just assume that code that may throw is a throw expression.
Clearly this function is not correct: our one requirement is that the boundary_function must not leak any exceptions, but the code that may throw may throw an exception. How do we catch exceptions? With a try/catch block, of course:
extern "C" HRESULT boundary_function() { try { // ... code that may throw ... return S_OK; } catch (...) { return E_FAIL; } }
This implementation is correct: the code that may throw is contained in a try block that catches all exceptions, so this function will not leak any exceptions to its caller. This implementation isn’t very caller-friendly, though, as it always reports a generic E_FAIL error code on failure, which is not very useful. This exception boundary is easily customizable, though, as we can add individual catch clauses to handle specific types of errors differently.
For discussion purposes, let’s say that our library uses its own exception type internally for failures, named my_hresult_error. In addition, our library makes use of new and delete, so we may also need to handle std::bad_alloc at the boundary. We don’t expect any exceptions other than these at the boundary, so for all other exceptions, we want to immediately terminate because we do not know what the state of the system is. Here’s what our updated implementation might look like with these constraints:
extern "C" HRESULT boundary_function() { try { // ... code that may throw ... return S_OK; } catch (my_hresult_error const& ex) { return ex.hresult(); } catch (std::bad_alloc const&) { return E_OUTOFMEMORY; } catch (...) { std::terminate(); } }
Every library may need to deal with different exception types, so the actual list of exception types to be handled and the way in which they are handled will differ across different libraries.
A colleague of mine noted that the std::system_error exception type is most useful for encapsulating error code and category information for failed system calls and other common errors. He provided the common example of what a handler for this exception might look like for our boundary_function:
catch (std::system_error const& e) { if (e.code().category() == std::system_category()) return HRESULT_FROM_WIN32(e.code().value); if (e.code().category() == hresult_category()) return e.code().value; // possibly more classifiers for other kinds of system errors: return E_FAIL; }
(I’ve omitted this from the main example solely for brevity, since we’ll be gradually modifying it through the rest of this article.)
It should be obvious that we can customize the exception-to-error-code translation however we need. There’s just one problem: the exception-to-error-code translation here is not reusable. Usually we’ll have more than one boundary function, and all of those boundary functions will usually require the same exception translation logic. We definitely don’t want to copy and paste this code all over the place.
Macros to the Rescue?
Macros are best avoided most of the time, but if they’re good for anything, then they’re good for stamping out code repeatedly. It’s pretty easy to encapsulate the catch clauses within a macro, then use that macro within our boundary functions:
#define TRANSLATE_EXCEPTIONS_AT_BOUNDARY \ catch (my_hresult_error const& ex) { return ex.hresult(); } \ catch (std::bad_alloc const&) { return E_OUTOFMEMORY; } \ catch (...) { std::terminate(); } extern "C" HRESULT boundary_function() { try { // ... code that may throw ... return S_OK; } TRANSLATE_EXCEPTIONS_AT_BOUNDARY }
This is certainly an improvement over having to copy-and-paste the catch clauses into every boundary function. There’s still a little boilerplate, but it’s quite reasonable. This solution isn’t terrific, though. It’s rather opaque, since the try is still present in the function but the catch clauses are hidden within the macro definition. It can also be difficult to debug through macro-generated code.
This solution isn’t awful, but we can do better…
A Translation Function
What’s better than a macro? How about a function? We can write a function to encapsulate the translation that we do in the catch clauses. I was first introduced to this technique at C++Now 2012 in Jon Kalb’s “Exception Safe Coding in C++” talk (linked above). The solution for our boundary_function looks something like this:
inline HRESULT translate_thrown_exception_to_hresult() { try { throw; } catch (my_hresult_error const& ex) { return ex.hresult(); } catch (std::bad_alloc const&) { return E_OUTOFMEMORY; } catch (...) { std::terminate(); } } extern "C" HRESULT boundary_function() { try { // ... code that may throw ... return S_OK; } catch (...) { return translate_thrown_exception_to_hresult(); } }
In this implementation, our boundary_function catches all exceptions and then, within the catch-all catch block, calls our exception translation function. Inside of the translation function, we make use of a nifty feature of C++: a throw with no operand will re-throw the current exception, that is, the exception that is currently being handled. This form of throw without an operand may only be used within a catch block—directly or, as is the case here, indirectly. Once the exception is re-thrown, we can handle it just like we would have handled it directly in the boundary_function.
This is a very clean technique for consolidating exception translation logic without the use of macros and with only a small amount of boilerplate in each boundary function. There is the slight disadvantage that the exception is re-thrown, so if you’re debugging with first chance exception breaking enabled, the debugger will break twice—once at the source throw, and once at the boundary translation throw. There is also some overhead with throwing twice, though in practice this is likely not a problem since the overhead is only incurred on the exceptional code path.
For more detailed information about this technique, take a look at the article “Using a Lippincott Function for Centralized Exception Handling,”, written by Nicolas Guillemot last month. I came across his article while researching for this article, and he goes into more technical detail on this technique than I do here.
[Aside: Our translation function should be declared noexcept; I’ve omitted it only because Visual C++ 2013 does not support noexcept.]
Lambda Expressions Make Everything Wonderful
The translation function may be very nice, but there is an even cleaner and simpler solution using C++11 lambda expressions. Let’s take a look:
template <typename Callable> HRESULT call_and_translate_for_boundary(Callable&& f) { try { f(); return S_OK; } catch (my_hresult_error const& ex) { return ex.hresult(); } catch (std::bad_alloc const&) { return E_OUTOFMEMORY; } catch (...) { std::terminate(); } } extern "C" HRESULT boundary_function() { return call_and_translate_for_boundary([&] { // ... code that may throw ... }); }
In this implementation, our boundary_function is quite simple: it packages up the entire body of the function, including the code that may throw, into a lambda expression. It then takes this lambda expression and passes it to our translation function, call_and_translate_for_boundary.
This translation function template takes an arbitrary callable object, f. In practice, the callable object will almost always be a lambda expression, but you could also pass a function pointer, a function object, or a std::function. You can pass anything that can be called with no arguments.
The translation function template calls f from within a try block. If f throws any exceptions, the translation function handles them and converts them to the appropriate HRESULT, just as we’ve done in the past few examples.
This technique is the least invasive and requires the least amount of boilerplate. Note that we’ve even been able to encapsulate the return S_OK; for the successful return case. To use this technique, we simply need to wrap the body of each boundary function in a lambda expression and pass that lambda expression to the exception translator.
Note that the lambda expression never needs to take any parameters itself; it should always be callable with no arguments. If the boundary function has parameters, then they will be captured by [&]. Similarly, for member function boundary functions, the this pointer is captured and other members may be accessed from within the lambda expression.
[Edited January 20, 2016: The original version of this article asserted that there is no overhead with this approach. It is true that there should be no overhead with this approach. However, at this time, the Visual C++ compiler is unable to inline functions that contain try blocks, so use of this approach will lead to a small amount of overhead in the form of an extra function call to the call_and_translate_for_boundary function.]
I first learned of this lambda-based technique whilst working on the Visual Studio IDE in C#. The Visual Studio SDK has a function ErrorHandler.CallWithComConvention() that performs exception-to-HRESULT translation and is often used by Visual Studio extensions for implementing COM interfaces using managed code. I later adapted this technique myself for use when implementing Windows Runtime components using WRL, and have found it to be invaluable.
Finishing Up…
We can’t use modern C++ everywhere, but we should use it wherever we can. These techniques presented here should help you to maintain clean boundaries between your code that uses exceptions and your APIs that must not leak exceptions.
While we’ve considered just one simple example involving a C function that returns an HRESULT, remember that these techniques are applicable to practically any kind of API boundary that is also an exception boundary. They work equally well for C functions, COM components, WinRT components implemented using WRL, etc. The return type needs not be an HRESULT: it could be a bool (success/failure) or an errno_t or an error code enumeration specific to your library or application. Finally, and most importantly, these techniques can be extended to support whatever sets of exceptions your library or component uses.
Next time, we’ll take a look at the other side of this problem: In code that primarily uses exceptions for error handling, how can we most effectively make use of APIs that report failure via error codes?
James McNellis is a senior engineer on the Visual C++ Libraries team, where he maintains the Visual C++ C Standard Library implementation and C Runtime (CRT). He tweets about C++ at @JamesMcNellis.
Special thanks to Gor Nishanov and Sridhar Madhugiri for reviewing this article.
Edit: Shortly after I posted this article, it was brought to my attention that this subject has been covered previously in a previous article, “Exception Boundaries: Working with Multiple Error Handling Mechanisms,” by David Blaikie.
0 comments