Partners: KDAB Whole Tomato Software CppDepend

21 May 2018

Error Handling and std::optional

Error handling and std::optional

In my last two posts in the C++17 STL series, I covered how to use std::optional. This wrapper type (also called “vocabulary type”) is handy when you’d like to express that something is ‘nullable’ and might be ‘empty’. For example, you can return std::nullopt to indicate that the code generated an error… but it this the best choice?

What’s the problem

Let’s see an example:

struct SelectionData
{
    bool anyCivilUnits { false };
    bool anyCombatUnits { false };
    int numAnimating { 0 };
};

std::optional<SelectionData> 
CheckSelection(const ObjSelection &objList)
{   
    if (!objList.IsValid())
        return { };

    SelectionData out;   

    // scan...

    return {out};
}

This code comes from my older post about refactoring with std::optional.

The basic idea is that if the selection is valid, you can perform a scan and look for “civil units”, “combat units” or a number of animating objects. Once the scan is complete, we can build an object SelectionData and wrap it with std::optional. If the selection is not ready, then we return nullopt - empty optional.

While the code looks nice, you might ask one question: what about error handling?

The problem with std::optional is that we lose information about errors. The function returns a value or something empty, so you cannot tell what went wrong. In the case of this function, we only had one way to exit earlier - if the selection is not valid. But in a more complicated example, there might be a few reasons.

What do you think? Is this a legitimate use of std::optional?

Let’s try to find the answer.

The Series

This article is part of my series about C++17 Library Utilities. Here’s the list of the other topics that I’ll cover:

  • Refactoring with std::optional
  • Using std::optional
  • Error handling and std::optional (this post)
  • Using std::variant
  • Using std::any
  • In place construction for std::optional, std::variant and std::any
  • Using std::string_view
  • C++17 string searchers & conversion utilities
  • Working with std::filesystem
  • Something more? :)

Resources about C++17 STL:

Error handling

As you might already know there are a lot of ways to handle errors. And what’s even more complicated is that we have different kinds of errors.

In C++, we can do two things:

  • use some error code / special value
  • throw an exception

of course with a few variations:

  • return some error code and return a computed value as an output parameter
  • return a unique value for the computed result to indicate an error (like -1, npos)
  • throw an exception - since exceptions are considered “heavy” and add some overhead a lot of projects use them sparingly.
    • plus we have to make a decision what to throw
  • return a pair <value, error_code>
  • return a variant/discriminated union <value, error>
  • set some special global error object (like errno for fopen) - often in C style API
  • others… ?

In a few papers and articles I’ve seen a nice term “disappointment” that relate to all kind of errors and “problems” that code might generate.

We might have a few types of disappointments:

  • System/OS
  • Serious
  • Major
  • Normal
  • Minor
  • Expected / probable.

Furthermore, we can see the error handling in terms of performance. We’d like it to be fast and using some additional machinery to facilitate errors might not be an option (like in the embedded world). Thus, for example, exceptions are considered “heavy” and usually not used in low-level code.

Where does std::optional fit?

I think, with std::optional we simply got another tool that can enhance the code.

std::optional Version

As I noted several times, std::optional should be mainly used in the context of nullable types.

From the boost::optional documentation: When to use Optional

It is recommended to use optional<T> in situations where there is exactly one, clear (to all parties) reason for having no value of type T, and where the lack of value is as natural as having any regular value of T.

I can also argue that since optional adds a “null” value to our type, it’s close to using pointers and nullptr. For example, I’ve seen a lot of code where a valid pointer was returned in the case of the success and nullptr in the case of an error.

TreeNode* FindNode(TheTree* pTree, string_view key)
{   
    // find...
    if (found)
        return pNode;

    return nullptr;
}

Or if we go to some C-level functions:

FILE * pFile = nullptr;
pFile = fopen ("temp.txt","w");
if (pFile != NULL)
{
    fputs ("fopen example",pFile);
    fclose (pFile);
}

And even in C++ STL we return npos in the case of failed string searches. So rather than nullptr it uses a special value to indicate an error (maybe not a failure but a probable situation that we failed to find something).

std::string s = "test";
if(s.find('a') == std::string::npos)
    std::cout << "no 'a' in 'test'\n";

I think that in the above example - with npos, we could safely rewrite it to optional. And every time you have a function that computes something and the result might be empty - then std::optional is a way to go.

When another developer sees a declaration like:

std::optional<Object> PrepareData(inputs...);

It’s clear that Object might sometimes not be computed and it’s much better than

// returns nullptr if failed! check for that!
Object* PrepareData(inputs...);

While the version with optional might look nicer, the error handling is still quite “weak”.

How about other ways?

Alternatively, if you’d like to transfer more information about the ‘disappointments’ you can think about std::variant<Result, Error_Code> or a new proposal Expected<T, E> that wraps the expected value with an error code. At the caller site, you can examine the reason for the failure:

// imaginary example for std::expected
std::expected<Object, error_code> PrepareData(inputs...);

// call:
auto data = PrepareData(...);
if (data) 
    use(*data);
else 
    showError(data.error());

When you have optional, then you have to check if the value is there or not. I like the functional style ideas from Simon Brand where you can change code like:

std::optional<image_view> get_cute_cat (image_view img) {
    auto cropped = find_cat(img);
    if (!cropped) {
      return std::nullopt;
    }

    auto with_sparkles = make_eyes_sparkle(*with_tie);
    if (!with_sparkles) {
      return std::nullopt;
    }

    return add_rainbow(make_smaller(*with_sparkles));
}

Into:

tl::optional<image_view> get_cute_cat (image_view img) {
    return find_cat(img)
           .and_then(make_eyes_sparkle)
           .map(make_smaller)
           .map(add_rainbow);
}

More in his post: Functional exceptionless error-handling with optional and expected

New proposal

When I was writing the article Herb Sutter published a brand new paper on a similar topic:

PDF P0709 R0 - Zero - overhead deterministic exceptions: Throwing values.

It will be discussed in the next C++ ISO Meeting in Rapperswil at the beginning of June.

Herb Sutter discusses what the current options for error handling are, what are their pros and cons. But the main things is the proposal of throws a new version of exception handling mechanism.

This proposal aims to marry the best of exceptions and error codes: to allow a function to declare that it
throws values of a statically known type, which can then be implemented exactly as efficiently as a return value.
Throwing such values behaves as if the function returned union{R;E;}+bool where on success the function returns the normal return value R and on err or the function returns the error value type E, both in the same return channel including using the same registers. The discriminant can use an unused CPU flag or a register.

For example:

string func() throws // new keyword! not "throw"
{
    if (flip_a_coin()) throw
        arithmetic_error::something;

    return xyzzys + plover”; // any dynamic exception 
                                // is translated to error
}

int main() {
    try {
        auto result = func();
        cout << success, result is:  << result;
    }
    catch(error err) { // catch by value is fine
        cout << failed, error is:  << err.error();
    }
}

In general, the proposal aims for having an exception-style syntax, while keeping the zero-overhead and type safety.

Consistency & Simplicity

I believe that while we have a lot of options and variations on error handling the key here is “the consistency“.

If you have a single project that uses 10 ways of error handling it might be hard to write new parts as programmers will be confused what to use.

It’s probably not possible to stick to the single version: in some critical performance code exceptions are not an option, or even wrapper types (like optional, variant, expected) are adding some overhead. Keeping the minimum of the right tools is the ideal path.

Another thought on this matter is how your code is clear and straightforward. Because if you have relatively short functions that do only one thing, then it’s easy to represent disappointments - as there are just a few options. But if your method is long, with a few responsibilities, then you might get a whole new complexity of errors.

Keeping code simple will help the caller to handle the outcome in a clear meaner.

Sorry for a little interruption in the flow :)
I've prepared a little bonus if you're interested in C++17, check it out here:

Wrap up

In this article, I reviewed some of the options to handle errors (or disappointments) in our C++ code. We even looked at the future when I mentioned new Herb Sutter’s proposal about “Zero-overhead deterministic exceptions”.

Where does std::optional fit?

It allows you to express nullable types. So if you have a code that returns some special value to indicate the result of the computation failure, then you can think about wrapping it with optional. The key thing is that optional doesn’t convey the reason for the failure, so you still have to use some other mechanisms.

With optional you have a new tool to express your ideas. And the key here, as always, is to be consistent and write simple code, so it doesn’t bring confusion to other developers.

What’s your opinion about using optional for error handling?
Do you use it that way in your code?

See previous post in the series: Using C++17 std::optional

Here are some other articles that might help:

And also here a presentation from Meeting C++ 2017 about std::expected:

Get my free ebook about C++17!

More than 50 pages about the new Language Standard.

C++17 in detail, by Bartlomiej Filipek

© 2017, Bartlomiej Filipek, Blogger platform
Any opinions expressed herein are in no way representative of those of my employers.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.