2

To hone my skills - and for the pleasure of it - I am writing a small game for my kids in modern C++ (C++11, C++14 and the part of C++17 already supported by Visual Studio), which is a nice break from my usual (enterprise) programming tasks.

I need a small interpreter for user input and of course I must handle erroneous input (think "csat magic missile" instead of "cast magic missile"). This is not exceptional, rather it is the norm that user input can be for some reason not well formed.

So I am looking for the recommended approach for handling this. I have read the C++ Core Guidelines or other questions on the site, and I have experimented with various approaches in my code.

In the end I have settled on changing all the relevant return types to something like

std::tuple<my_true_return_type, RetCode> my_function(...);

where RetCode is an enum:

enum RetCode{
SUCCESS,
WRONG_NUMBER_OF_INPUTS,
....
};

and each function call is done like:

std::tie(result, error_code) = my_function(...);

This seems more or less in line with the guidelines and I can be rather systematic with it.

My only issue with this solution is that it may happen that I am able to detect an error before even constructing my_true_return_type and in this case I have to default construct it anyway, only to discard it at the calling point.

Something like

if (an_erroneous_condition){
    // I have to default-construct a return type object, 
    // which I didn't really need 
    return std::make_tupe(my_true_return_type(), FAILURE); 
}

I could instead go for a Nullable return type (like the Maybe monad in Haskell), but at some cost.

In that case, I would have a relatively complex return type:

std::tuple<SomeNullableType<my_true_return_type, ...>, RetCode>

and the resulting complexity at the calling point. Furthermore there is not yet a nullable type in the standard library AFAIK, or in Boost (there has been the proposal of Boost.Outcome but is still under evolution), so I would need to find another solution, or roll my one which is fun, too, but doesn't seem stricly needed for the purpose of coding a simple game. And in all probability my solution would be half-baked, not production ready.

If I go back to the exception route I get a simplified return code and I don't have to construct any object which I don't need, but it seems to me that exceptions are to be reserved for exceptional cases and not for what I expect to be very common occur

So my question is:

  • how to handle, according to current best practice, and taking full advantage of all modern C++ technique, errors which are not exceptional situations?
  • if my way is reasonable, what can I do to ensure that the cost of the "useless" default constructed return type is minimized?
  • I'm by no means a C++ expert, but I think the answer depends on how much granularity you need. Do you need to know exactly what type of problem happened or is it enough that you had an erroneous result? If you just need an erroneous flag, the nullable type alone would be enough. Whatever you choose, pick the simplest thing that can work. – Berin Loritsch Aug 15 '17 at 16:21
  • I would like to mantain granularity, as I am looking to refine my skills I am looking for the recommendee best practice. – Francesco Aug 15 '17 at 16:23
  • 1
    The standard library does have nullable types: std::unique_ptr, at the cost of only one extra dynamic memory allocation in case you instantiate a result. I use it all the time to signal presence/absence of a value while also managing ownership correctly (so this is better than plain pointers). – amon Aug 15 '17 at 17:48
  • 5
    Visual Studio's C++17 support has std::optional and std::variant (at least in VS 2017). There is a proposed std::expected which may behave more like what you want; Boost Outcome implements something similar to it: github.com/ned14/outcome – Justin Aug 15 '17 at 18:02
8

Boost::variant and Boost::optional would be the libraries I recommend looking at. They are tremendously effective at minimizing the extra costs of construction because they wont initialize a member unless there's data to initialize it with.

That being said, I would look at this from a higher level point of view. Error handling is often terse, and it is a major complaint among users. If it isn't terse, it's verbose! (anyone try to debug a boost template error that's 100 lines long?!)

Exceptional errors are handled tersely because they are exceptional. The developer cannot be expected to flesh out how all of the exceptional errors should be handled. Instead, they are rewarded for casting a wide net, catching lots of different errors, and then trying to return to a reasonable sane state before continuing.

IF you have non-exceptional errors, then that means you're thinking of them as part of your program's flow. Give them the credit they deserve. Don't just ask "how can I catch these errors," but ask "how would the user want to handle a mistake like this?"

I often write parsers where providing a filename and line number is the difference between a hateful customer and an appreciative one. So I make sure all of my error handling code can pass filenames and line numbers through them, even going so far as to make a "stack trace" of objects being manipulated at the moment the error occurred.

In the case of user input, the user likely not only wants to know that an error occurred, but have some feedback suggesting what the computer thinks the user wanted to do in the first place. If you typed csat magic missile, the user may appreciate you passing around enough information to realize csat is the offending word, and that it has a very short hamming-distance from cast, so it was likely a typo. This means you will likely want to be able to pass the incompletely parsed data into a smart algorithm that can try to heuristically guess what the user was thinking.

Once you start thinking that way, you'll find that many of the technical details such as constructors and memory utilization solve themselves. Now, instead of error handling being "that thing we do because we have to," it's a vibrant part of your program.

  • Are you sure you want hamming-distance instead of a more flexible edit-distance? – Deduplicator Aug 15 '17 at 18:03
  • @Deduplicator Nope! Not sure at all! Sounds like the OP should try several different approaches and see which works best =) (Nice spy, by the way.) – Cort Ammon Aug 15 '17 at 18:07
  • Levenshtein distance has worked well for me in the past (Damerau-Levenshtein distance would probably be even better) – Justin Aug 15 '17 at 18:14
  • To be honest I already embrace the view that error handling is part of the control flow and not something to be done because it has to be done, hence my quest for the best modern approach – Francesco Aug 15 '17 at 18:49
2

The purpose of throwing an exception is to signal that a function cannot return a valid result. As such, exceptions are an essential part of modern C++. They have costs and benefits both in code complexity and runtime speed that need to be evaluated for each situation. I think exceptions are a net gain for your program due to the simplicity of the code compared to other options.

If I go back to the exception route I get a simplified return code and I don't have to construct any object which I don't need, but it seems to me that exceptions are to be reserved for exceptional cases and not for what I expect to be very common occur.

The first half of this sentence contains great reasons for using exceptions.

As for the second half, I've seen two reasons for only throwing exception in rare--truly exceptional--cases:

  1. Compilers optimize for the exception-free path, so throwing exceptions as part of normal running can severely slow down your program.

  2. Exception handling requires a lot of infrastructure: try{...}catch(...){...}. This can be annoying for simple cases. See Eric Lippert's essay on vexing exceptions.

The first point is not relevant in your case since you are waiting for user input. There's nothing for the program to do until the user gives a valid command, so there's no need to worry about performance here.

The second point, I believe, is only relevant for simple situations where you only care whether or not my_function(...) succeeded, not why it failed. As an example, here's how to repeatedly ask the user for input until a valid integer is entered using the exception-throwing int32.Parse():

int32 result;
while(true)
{
    try
    {
        result = int32.Parse(getUserInput());
        break;
    }
    catch{int32.ParseException)
    {
    }
}

Compare this to the non-throwing int32.TryParse() version:

int32 result;
while( ! int32.TryParse(getUserInput(), result));

Much nicer! This is why the exception thrown by int32.Parse() is called "vexing" in the linked essay.

In your case, you probably want the program to help the user diagnose the problematic input and suggest corrections. This is a complex procedure deserving of its own block of code. With exceptions, you can throw different kinds of exceptions (corresponding to your RetCodes) and use different catch-blocks to handle different kinds of errors. You're going to have this infrastructure no matter what error-handling strategy you use.

  • 1
    Thanks for the link to Eric Lippert's essay. It validates my reasoning that in this case throwing would be improper ("vexing", as he says). – Francesco Aug 16 '17 at 9:17
1

In my AvCpp (FFmpeg wrapper) project I select next strategy: use OptionalErrorCode class that wraps std::error_code and use it as last optional function/method argument (other solution: use two overloads methods with and without error storage argument: in such case I recommend to use error storage as a first argument).

Logic is simple: if something fail and user provides storage for error code - error code stores in this storage. Otherwise exceptions is throws. On systems where exception is impossible for some reasons (in my case I have only 300 Kb for code ;-)) exceptions throwing can be replaced by some sort of panic: abort()/std::terminate()/just device reset or so on.

In my case it simplify API usage but provides quick reaction on fail if user omit error handling.

Also, in such strategy we can return object in any state: we just provide guarantee that object in valid state only if no error. Otherwise it state is unspecific.

Of course, if you provide error storage and omit checking... If anybody want to exterminate itself he do it in any case.

You can see implementation:

UPD: C++20 can allow us to use structure binding like C99 in such case we can use some sort of naming arguments that can improve error handling described above:

struct SomeFunctionArgs {
  int arg0;
  int arg1;
  int arg2 = 31337;
  OptionalErrorCode ec = OptionaErrorCode::null();
};

// Function
SomeObject get_some_object(SomeFunctionArgs&& args) {
  ...
}

// Call
auto obj0 = get_some_object({.arg0 = 1, .arg1 = 2}); // throw exception if fail

std::error_code ec;
auto obj1 = get_some_object({.arg0 = 1, .arg1 = 2, .ec = ec}); // fill ec with error code, no throw. Optional arg2 is not touched.
1

There is some religiosity in the matter of error codes versus exceptions, which makes best practices tricky to establish. A case can be made for both in this question.

"Exceptional" doesn't mean "happens rarely" so much as "outside the algorithm's purview"; I'd argue that non-well-formed expressions are exceptional to a parser. Additionally, in the C++FAQ answer following the one linked to in the question, Stroustrup states:

In C++, exceptions is [sic] used to signal errors that cannot be handled locally[...]

which seems to describe the case of uncorrectable (if including Cort's excellent suggestion) parse errors (though even correctable parse errors might cause an exception, to cause the corrected command to get verified rather than blindly executing it).

Common arguments against exceptions (usually in support of error codes) involve performance, design leakage, and complexity (along the lines of "spaghetti code", as control flow may "invisibly" jump over code blocks).

  • performance: failing at the first input error, there will be at most one parse error per line of input. Since exceptions are limited by user input, which is orders of magnitude slower than the cost of exceptions, performance cost is negligible. Also, the C++FAQ linked in question includes Stroustrup's rebuttal:

    but exceptions are expensive!: Not really. Modern C++ implementations reduce the overhead of using exceptions to a few percent (say, 3%) and that's compared to no error handling. Writing code with error-return codes and tests is not free either. As a rule of thumb, exception handling is extremely cheap when you don't throw an exception. It costs nothing on some implementations. All the cost is incurred when you throw an exception: that is, "normal code" is faster than code using error-return codes and tests. You incur cost only when you have an error.

  • leakage: leakage occurs when something passes a boundary and is considered harmful when it breaks principles such as Separation of Concerns. Command line handling is part of the user input module; as long as the parse exception is handled within the same module, there is no cross-concern leakage. Also, error codes can represent the same kind of leakage (information about failure modes), and thus aren't any more appropriate in this regard.

    Another boundary exceptions are accused of crossing is the one between implementation and interface. The usual response to this is "exceptions are part of the interface, explicitly in languages with throw declarations and implicitly in others as they represent another return type." At this point, matters can become religious.

  • complexity: exceptions involve a kind of control flow complexity, as they have an impact on control, and code complexity, in the form of exception handlers. Error codes also introduce control flow and code complexity in the form of branches, in some cases nested branches. (There's an illustration of complexity involving diagramming with triangles the whitespace arising from branch indentation that's very relevant, but I can't recall the reference.)

    Code blocks that are potentially skipped over by exceptions are allowed to assume a valid state, which simplifies those code blocks (they're only skipped if state is invalid), whereas error codes and invalid state must be handled at every level with error codes. The latter could be an advantage, if it's important to see the error state at every level, though whether this importance exists can be a religious question.

An argument in favor of error codes is that, since they must be handled at every level, there won't be any missing information, which isn't the case with exceptions.

Matters can get more confused (or perhaps clarified) when you examine the alternate-type return error code pattern (especially using an Either type, but including using a pair) and realize it can be implemented equivalent to exceptions, though at a program level rather than language level. It's a standard way exceptions are implemented in functional languages, using a type that has features of the nullable type class (operations on the null value get absorbed) but that has multiple values (error messages + stack traces + other useful data) for the error type. From a conceptual view, the two aren't that different. In the final analysis, errors and the need for error handling are what introduce complexity, not any particular error handling approach (though some approaches may be the simplest, depending on the nature of the errors and normal processing).

On the matter of useless objects, avoiding object construction comes from an old stance. You generally don't need to avoid object construction for performance reasons (however, see the next paragraph), especially as construction of default objects often has a low enough impact; it should be possible to implement classes that encapsulate user commands with low impact default construction.

Ultimately, the best practice is to start with whichever approach is simplest to test, implement & maintain and make adjustments only if performance doesn't meet non-functional requirements and profiling reveals the error-handling approach has a significant impact.

1

If I go back to the exception route I get a simplified return code and I don't have to construct any object which I don't need, but it seems to me that exceptions are to be reserved for exceptional cases and not for what I expect to be very common occur

I've headbutted so many developers on this topic. I'ma getting stressed out just thinking about it! :-D

If you are propagating error codes down the call stack like a C programmer in C++ (though I do love C, but that's a different language in my book, and to a large degree because the introduction of EH shifts the whole way we deal with resource management) because you don't consider some branch case "exceptional enough", then religious zeal over human interpretations of words has replaced productivity at that point.

In particular I clashed with multiple devs because I wanted to throw when a user hit an abort button on an operation accompanied by a progress bar. And they did not consider that "exceptional enough" so as to religiously replace my exception-handling code with code littered all over the place like:

if (err == user_abort)
    return err;

Only for the caller to repeat the same code all over the freaking place with no perceivable performance benefits to the time it takes to abort the operation only for a try/catch block in the operation to still be required anyway for other exceptional cases. I was like, "Thank you so much for degrading our productivity and making the code much more prone to human error by reinventing what exception handling already solves! (... idiots!)" :-D *

  • I don't understand how pencil necks with PhDs in computer science can be so stupidly counter-productive and so brilliant at the same time. I suggest they get a refund on their student loans and try to actually ship something on their own.

My pragmatic and blunt view on exception-handling is that if you are writing error propagation code of the kind that exception-handling is intended to avoid in places where the time to throw isn't even perceivable to the user, then either you have genuine ABI concerns like throwing across module boundaries, or you're a counter-productive zealot.

So I would just throw in these cases, unless you have things like module boundary or genuine (measured) performance concerns. The litmus test over dissecting the human ideas of "exceptional execution paths" (which can get into a boatload of subjectivity) should be to consider practical performance and what sort of code you would write alternatively lacking use of exceptions.

how to handle, according to current best practice, and taking full advantage of all modern C++ technique, errors which are not exceptional situations?

Your case is exceptional in my book. It doesn't necessarily cost that much to throw (throwing is expensive, relatively speaking, in some granular loopy contexts, but not relatively for a syntax error interpreting code), and the alternative code you would write resembles C-style error handling and propagation of error codes. Done. Use EH and gitter shipped.

What I don't consider truly exceptional is some use case which isn't due to some external input (file, server, memory allocator, user, whatever -- something outside of the developer's control) providing something other than expected, like a map find method which doesn't find a key being searched (it's not even necessarily "external" as the client might have built that map themselves with the expectation that they might search for keys which may not exist). That isn't necessarily an exceptional case, as the user/client/consumer could write the code with the expectation that the key may not exist. In that case you can just return an invalid iterator or null pointer or whatever.

And if that's still vague, then just consider what sort of alternative code you'd write to use it without exceptions, and possibly measured performance. If it's starting to resemble C error-handling code detecting and propagating errors multiple levels down the call stack, then you're probably dogmatically depriving yourself of the benefits of exception-handling.

For programmer errors an assert often suffices, unless you're working in such a mission-critical field that even bugs that escape the radar of automated testing should lead to a graceful recovery to avoid killing users or something.

if my way is reasonable, what can I do to ensure that the cost of the "useless" default constructed return type is minimized?

Again your case is exceptional in my book. Especially with "zero-cost" exception handling, if returning extra data to indicate what went wrong even in the successful cases is taking a performance hit, then exceptions solve that problem by shifting the cost away from "successful" (and I think "successful" is clearer wording than "normal") branches of execution in exchange for steeper costs to "failed" paths of execution.

Do you want to optimize your interpreter to run many lines of correct code as fast as possible or report syntax errors a millisecond (or whatever) faster in exchange for slowing down interpreting correct code?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.