C++ Exception Specifications

I am not convinced that going from dynamic exception specifications (“throw (T1, …)”) to noexcept specifications in C++11 is necessarily a good thing.  I understand the rationale behind http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3051.html, but feel like it is throwing out the baby with the bathwater.

Java brought forward the idea of distinguishing between checked and unchecked exceptions, and I think there is merit to the underlying idea that there are exceptional conditions that are intended to be “handled locally” vs. ones that necessarily make an application “throw up its hands” anyway.

Incidentally, this dichotomy is evident in C++’s throwing vs. non-throwing operator new variants—std::bad_alloc from “new Foo(bar, baz)” somewhere in an application’s plumbings can hardly be countered more meaningfully than by aborting the application.  But when the application reads some file format, say, and in doing so needs to allocate a buffer whose size is determined by the client-supplied input file, responding to a null “new(std::nothrow) char[n]” with a “bad input file” message and then carrying on with the next input file is likely appropriate.

Where that second case can of course also be coded with the throwing operator new variant and a catch-block for std::bad_alloc.  Which brings us to the question what constitutes an “exceptional condition” that should be responded to by throwing an exception vs. what should be handled by other means of error reporting, like returning an error code.

The question is somewhat philosophical, the answer likely depends on context (see the operator new examples), and there is enough gray area to facilitate endless bikesheds.  In other words: insignificant in practice.

What is significant in practice, though, is the observation that C++’s lack of decent support for algebraic data types (in particular, sum types) can make an argument for using exceptions in lieu of other error reporting channels even in cases where the philosophical answer would be “Don’t.”

Consider a function f that would normally return a value of type T, but, depending on input, can also fail with errors E1 or E2.  With algebraic data types, the return type would be the sum (aka discriminate union) type T | E1 | E2, say, and client code would handle the various cases with pattern matching.  Or, in situations where client code either can be certain that errors cannot occur for the given input or is happy to “throw up its hands” and not deal with errors other than by aborting the application, it would leave part of the pattern space unexplored, implicitly leading to abort if it ever is encountered.  See any text on functional programming for inspiration and detail.

Now, in C++, the way to code function f is with a return type T and a pointer to some E1-or-E2-or-neither flag as an additional out parameter (and documentation what the return value of type T is if the additional flag indicates an error).  Or, where that approach happens to work, with a return type T’ that leaves room to encode T along with E1 and E2 (e.g., using negative integers for errors when T would only encompass non-negative integers).  Or with some E1-or-E2-or-neither flag as return type and an additional out parameter of type T (and documentation what the state of the out parameter is when the return value indicates an error).  All very elegant.

Or with a return type T and an exception specification “throws (E1, E2)”.  And pattern matching coded as catch blocks.  Borders on elegant.  If only there were not all those gotchas.

For one, this would make any exceptions from the “unchecked” category go to std::unexpected.  Either leave it at that for “throwing up hands” anyway, or, where better composability is required (in library code, say), handle it via “throws (E1, E2, std::exception)” or similar (depending on how you model your exception types in relation to the standard exception hierarchy).

For another, the compiler does not aid you in finding places where you accidentally forgot to handle one of f’s error cases.  And static analyzers that do help you with that have a hard time avoiding false positives in places where you deliberately left out handling of an error case that cannot happen in that specific situation.  (That is, there is no elegant way to assert “throwing E1 cannot happen here” similarly to how a “case N: assert(false);” can do that in a switch.)

For yet another, dynamic exception specifications are deprecated anyway…

For exceptional conditions that shall be handled programmatically, it is important that the contracts of called functions with respect to those conditions are clear, and that contract violations by both caller and callee can be detected statically as much as possible.  But the approach taken by C++11 with noexcept(false) does nothing in that regard when modelling that kind of exceptional conditions with exceptions.

Advertisements

One thought on “C++ Exception Specifications

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s