DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Trending

  • Beyond Code Coverage: A Risk-Driven Revolution in Software Testing With Machine Learning
  • How Trustworthy Is Big Data?
  • How To Develop a Truly Performant Mobile Application in 2025: A Case for Android
  • Performance Optimization Techniques for Snowflake on AWS

Error Handling and std::optional

In this article, we'll review some of the options to handle errors (or disappointments) in our C++ code, and see where the std::optional wrapper fits in.

By 
Bartłomiej Filipek user avatar
Bartłomiej Filipek
·
Aug. 18, 18 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
9.8K Views

Join the DZone community and get the full member experience.

Join For Free

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?

The article comes from a blog - bfilipek.com - weekly articles about modern C++.

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.

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.
  • 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 relates 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 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 optionals, 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 and what their pros and cons are. 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 “xyzzy”s + “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 to have an exception-style syntax, while keeping the zero-overhead and type safety.

Consistency and 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 as to 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.

Wrapping 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 confuse 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

More from the Author:

Bartek recently published a book - "C++17 In Detail"- rather than reading the papers and C++ specification drafts, you can use this book to learn the new Standard in an efficient and practical way.

Published at DZone with permission of Bartłomiej Filipek, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!