In Ancient Times, I was privileged to be among those who deliberated upon a fundamental question in C++ programming philosophy: “Whither exception handling?” Exception handling was still in the conceptual stages then, and the air was charged with excitement.
Twenty two years ago, I wrote an article for Windows Tech Journal recounting my experience a few years earlier in comparing error handling mechanisms of exceptions vs. error code returns. In particular, I discovered that using both was an impedance mismatch, with code always wrapping the other kind to convert it: catch the exception and return an error code; or test for an error code and throw an exception. This blather is a road leading towards a pure exception handling approach.
I’ve scanned in the magazine pages and it can be seen on my web server.
Now, the C++ community is coming full circle it seems. Everything old is new again.
In the 2017 standard, we have
std::optional which is the most basic “sum type”. In a talk, Andrei Alexandrescu started with the example of the
variant and asked the audience “Why is it called a ‘sum type’?” My own answer: because you don’t know its type but it returns something!
Seriously, in this presentation, he also said “Remember when I told you the worst thing that happened to humankind was? I see the
std::optional.” He introduces the
expected class (still being argued about in committee as I write this), as the right solution giving the best of both worlds: It can return a result or an error code, and the caller can take the step of checking the error code or just access the result without checking, in which case it throws an exception if it’s actually an error state.
Meanwhile, Niall Douglas rips
expected. Based on the peer review results of
Boost.Outcome, he notes for consideration that many of the findings apply to the current expected proposal.
There’s No Substitute for Experience
This is part of a series of posts I’m writing about using toy projects and other exploration to get a hands-on feel for the new C++17 features, library, and style, as well as the behavior and real limitations of the compiler.
I coded up a very simple recursive descent parser, since it’s been noted that the new
std::string_view is especially suited for parsers. (There is a lot of sentiment about
string_view being problematic and evil, but that’s another story.)
Now a recursive descent parser is one of the few places it is generally acknowledged, where throwing an exception as a form of control flow is genuinely a good decision. But, this simple grammar doesn’t have enough depth to need anything like that.
The interpreter (it does the work as it parses, as opposed to building a tree representation to be evaluated later) throws exceptions in case of errors. The user of the class will know this as the way it gives errors on bad input or run-time conditions.
The parsing step itself, though, makes heavy use of
std::optional. As is the nature of such a parser, a production (grammar rule) might be called where this thing may or may not exist: optional parts in the syntax and alternatives and lists all lead to logic that says, “read one of these; did you get one? No? OK, skip it (or try reading one of those instead).”
Other callers need to read something, period. In that case, the caller needs to check for an error (just as it did when it was optional) and throw an exception. This code is what gave me déjà vu:
auto LHS= read_identifier(input);
if (!LHS) raise_error(input, 4);
read_required ('=', input);
auto terms_result= read_terms(input);
if (!terms_result) raise_error (input, 5);
if (!input.empty()) raise_error (input, 3);
set_value (*LHS, terms_result);
read_required throws an exception itself if it can’t do what was asked.
read_identifier, like most functions modeling grammar productions, return an
Call a function, then check the return value and throw. This is done repeatedly in these functions. That is exactly the kind of mismatch I saw all those years ago.
From the nature of the
optional returns, it is the caller who decides on the error code. In a more complex grammar, I can easily see wanting to propagate that or modify it as it passes back up the call tree. But in the case of
optional, there is no error code coming up — just the lack of a result.
In testing this with different syntax error test cases, I found places where I was not checking the return code. This can coredump because dereferencing the
optional does not do any checking on whether it contains a value. (On the other hand, there is a
value() member function that does check.) I guess I’m so used to writing such that functions do what they are asked (or don’t return at all) that writing in a style where every call is followed up by an explicit test is challenging as well as ugly and obfuscating.
It’s back to the assessment I made when promoting exceptions in the first place: look at this block of code — what does it do? Its main purpose is a bunch of
if statements and errors. Where is the real logic I came here for? The testing is drowning out the real work.
Would using expected/outcome be better for this?
If the called function loaded the result with an error code rather than just the lack of result, and the attempted dereference would throw an exception, then I would not have to check after every call but still could when I wanted to.
But… it is the caller that knows what error code to assign. Furthermore, what about the specifics of the exception and its payload? If productions returned an error code (just an enumeration), the exception still needs to be the
parser_error type and include the cursor position.
It's like the (deeply nested) called function needs to interact with the caller to formulate the proper error. Catching and modifying and re-throwing is something that will not go over well with modern implementations, as throwing is very slow.
I’m reminded of the exception class I wrote and used long before C++ was a standard: I included a feature to simulate dynamic scoped values-of-interest.
Parsing is one case where we really care about details across several levels of function call, in order to get meaningful feedback on the error. In more normal circumstances, you tend to rely on logging. Even so, having a speculative log — include this line if an error is logged; flush it upon success — is very helpful and cuts back on the spew of irrelevant information logged.
But the same technique could be used to generate a more meaningful exception, adding information as the stack is unwound.
Besides catch to deal with a problem after it occurs, and destructors to automatically clean up resources, we need some way to prepare for a possible error. This would be primarily for reporting, not for recovery actions. That’s probably why it has been neglected as a topic — destructors are perfectly good at cleanup and using them means we don’t have to explicitly prepare.