Table of Contents

In this article, we’ll go through a new vocabulary type introduced in C++23. std::expected is a type specifically designed to return results from a function, along with the extra error information.

Motivation  

Imagine you’re expecting a certain result from a function, but oops… things don’t always go as planned:

/*RESULT*/ findRecord(Database& db, int recordId) {
    if (!db.connected())
        return /*??*/
    
    auto record = db.query(recordId);
    if (record.valid) {
        return record;

    return /*??*/
}

What can you do? What should you return or output through /*RESULT*/ ?

Here are a few options:

  • Throw an exception,
  • Terminate the application,
  • Use std::optional to return a value or “nothing” to indicate an error,
  • Use a separate struct like my_func_result that would store the value and also an extra error message,
  • Use std::variant <ValueType, Error> so that it’s type-erased and occupies less space,
  • Use some ugly output pointer/reference to output the value and return an error code

In the case of findRecord teminating an application is probably an overkill. std::optional only indicates if we have a value or not, and that may not be enough for some scenarios.

That’s where std::expected comes to the rescue. It’s a clever way to say,

I’m expecting this value, but hey, I’m also prepared for something unexpected!

Here’s why std::expected is a great option:

  • Clearer Error Handling: using return codes or exceptions to handle errors is a common technique, but sometimes, this can make your code look a bit messy, or you might miss an error sneaking by. With std::expected, errors get their spotlight.
  • The right vocabulary type for the job: std::expected is something between std::optional and std::variant, but specifically designed to handle errors.
  • Rich Error Info: std::expected lets you pack a whole bunch of useful info into the result type.
  • Efficiency: std::expected is designed to be efficient, avoiding some of the performance hits you might get with exceptions. It’s also an excellent choice for environments where exceptions are not an option.
  • Familiarity: If you’ve worked with other languages like Rust or Haskell, std::expected will be very familiar to you. It’s similar to Rust’s Result or Haskell’s Either.

We can rewrite the initial findRecord function and apply std::expected:

std::expected<DatabaseRecord, std::string> findRecord(Database& db, int recordId) noexcept {
    if (!db.connected())
        return std::unexpected("DB not connected");

    auto record = db.query(recordId);
    if (record.valid)
        return record;

    return std::unexpected("Record not found");
}

int main() {
    Database myDatabase;
    auto result = findRecord(myDatabase, 1024);
    if (result)
        std::cout << "got record " << result.value().valid << '\n';
    else
        std::cout << result.error();
}

Run @Compiler Explorer.

In the above example, the function returns DatabaseRecord or std::string to indicate an error. If the result is invalid, then we can access the error message via the error() member function.

Use Cases  

Here are the places where std::expected can be used. I’d say there are three core use cases:

  1. Enhanced Function Return Types - For functions that might fail, use std::expected to return either a successful result or a detailed error, making the outcomes clear and explicit.
  2. Complex Process Error Handling - In multi-step processes where each step could fail, std::expected encapsulates the success or failure of each stage, simplifying error tracking and handling.
  3. Functional Programming Patterns - Thanks to monadic extensions, you can use functional programming styles for operations that might succeed or fail, similar to ‘Either’ in Haskell or ‘Result’ in Rust.

One comment is that std::expected is similar to Rust’s “Result” and I think the name “Result” conveys more information about where it’s used. As you can see, std::expected is closely related to returning results from functions. I don’t think we can/should use it as data members. For data members, it’s better to rely on std::optional or std::variant.

After some basic introduction, let’s have a look at some details,

Creating std::expected objects  

std::expected is quite flexible when it comes to creating and initializing its instances. Let’s have a look at the options:

Default Construction

A std::expected object can be default constructed. This initializes the object to contain the expected value, which is default constructed.

std::expected<int, std::string> result; 

The value stored is 0. See @Compiler Explorer.

Direct Initialization with Expected Value

You can directly initialize std::expected with a value of the expected type.

std::expected<int, std::string> success(42);

The value stored is 42. See @Compiler Explorer.

In-Place Construction of the Expected Value

For complex types, or when you want to directly construct the expected value in place, use std::in_place and provide the constructor arguments.

std::expected<std::vector<int>, std::string> success(std::in_place, {1, 2, 3});
std::expected<std::pair<bool, bool>, int> success(std::in_place, true, false);

See @Compiler Explorer

Initialization with an Unexpected Value

To represent an error or unexpected situation, you can initialize std::expected with std::unexpected. The argument passed to std::unexpected should be of the error type.

std::expected<int, std::string> error(std::unexpected("Error occurred"));

In-place Initialization with an Unexpected Value

There’s also an option to use in-place initialization of the error value:

std::expected<int, std::string> error{std::unexpect, "error"};

See @Compiler Explorer

Notice that std::unexpect is an instance of the type std::unexpect_t, and this is a very similar name to std::unexpected. Easy to confuse those types.

Copy and Move Construction

For completeness, std::expected objects can be copy and move-constructed.

std::expected<int, std::string> copy = success;
std::expected<int, std::string> moved = std::move(error); 

Assignment

std::expected supports both copy and move assignment. It can be assigned from values of the expected type or from std::unexpected.

std::expected<int, std::string> result;
result = 10; 
result = std::unexpected("New error");

Accessing the Stored Value or Error in std::expected  

Once you have a std::expected object, accessing its contained value or error is straightforward but flexible, allowing you to handle each scenario appropriately. Here are the primary ways to access the stored value or error, along with examples:

Checking for a Value

Before accessing the contained value, it’s wise to check whether the object holds the expected value or an error.

Using has_value() Method:

std::expected<int, std::string> result = getSomeResult();
if (result.has_value())
    std::cout << "Success: " << *result << '\n';
else
    std::cout << "Error: " << result.error() << '\n';

Implicit Conversion to bool:

std::expected can be implicitly converted to bool, indicating the presence of an expected value.

if (result)
    std::cout << "Success: " << *result << '\n';

Accessing the Expected Value

Using operator* or operator->: Directly dereference the std::expected object to access the expected value, assuming the value is present (UB if not).

std::cout << "Value: " << *result << '\n';

Using value(): This method returns a reference to the contained value. If the object holds an error, it throws std::bad_expected_access<E>.

int main() {
	std::expected<int, std::string> result{std::unexpected("")};

    try {
        std::cout << "Value: " << result.value() << '\n';
    } catch (const std::bad_expected_access<std::string>& e) {
        std::cout << "Caught error: " << e.what() << '\n';
    }
}

See @Compiler Explorer

Getting Value with a Default

Using value_or() Returns the contained value if available; otherwise, it returns a default value provided as an argument.

std::expected<int, std::string> result{std::unexpected("")};
int value = result.value_or(0); // Returns 0 if result holds an error
std::cout << "Value: " << value << '\n';

See @Compiler Explorer.

Accessing the Error

Using error()retrieves the stored error. The behavior is undefined if has_value() is true.

std::expected<int, std::string> result{std::unexpected("")};
if (!result)
    std::cout << "Error: " << result.error();

Modifying the Contained Value

If std::expected contains a value, you can modify it directly through a dereference.

std::expected<int, std::string> result{32};
if (result)
    *result += 10;

The same can be achieved via the .value() function, as it returns a non-const reference.

Modifying the Contained Error

There’s also an option to modify the error:

std::expected<int, std::string> result{std::unexpected{"err"}};
if (!result)
    result.error() += std::string{" in the filesystem"};

See @Compiler Explorer

Requirements on the Stored Value and Error Types in std::expected  

So far, the examples have used some regular types as a value or error, but let’s see some details on the exact requirements. In short, std::expected is designed to be flexible, but there are some constraints on the types it can hold:

  • The value type T should be an object type or void. This means fundamental types, classes, and pointers can be used, but functions, C-style arrays, and references cannot.
  • It should satisfy the Destructible requirements
  • std::expected does not allow T to be another std::expected type, std::in_place_t, or std::unexpect_t.

For example, you cannot use references, but std::reference_wrapper is fine:

//std::expected<int&, int> ptr2; // error
int v = 10;
std::expected<std::reference_wrapper<int>, int> ref{std::ref(v)};

Or you cannot use functions, but std::function is fine:

// std::expected<void(), std::string> invalidFunction; // err

// fine:
std::expected<std::function<void()>, std::string> validFunction;
validFunction = []{};

You cannot use std::unexpected as the expected type:

// std::expected<std::unexpected<std::string>, std::string> invalidExpected; // err!

As for arrays. C-style arrays are invalid, but std::array is fine:

// std::expected<int[10], std::string> invalidArray; // err
std::expected<std::array<int, 10>, int> validArray; // fine

There’s also an intriguing case with void, as it can be used for the value type:

std::expected<void, std::string> performAction(bool condition) noexcept {
    if (condition)
        return {};
    else
        return std::unexpected("Action failed");
}

int main() {
    if (auto result = performAction(false); result)
        std::cout << "Action performed successfully\n";
    else
        std::cout << "Error: " << result.error() << '\n';
}

See @Compiler Explorer.

As for the Error Type:

  • The error type E must also be an object type and satisfy the Destructible requirements.
  • It should be a valid type for use with std::unexpected, and semantically meaning it should be suitable for holding error information. Typically, this can be an error code, a string, or a custom error class.
  • The error type E should not be an array type (C-style), a non-object type (like function types), or a cv-qualified (const or volatile) version of these.

Functional extensions  

In my article about How to Use Monadic Operations for `std::optional` in C++23 - C++ Stories we learned about functional/monadic extensions. And what’s cool is that in C++23, similar functionality is also applied to std::expected. If you know how to use it for std::optional, then you can also apply this knowledge to our expected type.

The feature was accepted as “Monadic operations for std::expected - P2505R5”.

Here are the member functions:

  • and_then()
  • transform()
  • or_else()
  • transform_error() (this one is not available for optional)

Those functions are a distinct and significant topic, so let’s wait for a separate article.

A basic example  

To conclude our journey, let’s pick a standard case of number conversion and let’s use std::expected to convey the error message. The small twist with this example is that I used std::string to pass the error, and I “translate” error codes into strings:

#include <charconv>
#include <expected>
#include <string>
#include <system_error> 
#include <iostream>

std::expected<int, std::string> convertToInt(const std::string& input) noexcept {
    int value{};
    auto [ptr, ec] = std::from_chars(input.data(), input.data() + input.size(), value);
    
    if (ec == std::errc())
        return value;

    if (ec == std::errc::invalid_argument)
        return std::unexpected("Invalid number format");
    else if (ec == std::errc::result_out_of_range)
        return std::unexpected("Number out of range");

    return std::unexpected("Unknown conversion error");
}

int main() {
    std::string userInput = "111111111111111";

    auto result = convertToInt(userInput);
    if (result)
        std::cout << "Converted number: " << *result << '\n';
    else
        std::cout << "Error: " << result.error() << '\n';
}

See @Compiler Explorer

While we could use std::optional, the version with std::expected conveys more information about the error.

Summary  

I hope you like this introduction to std::expected from C++23.

In this text, I showed you how to create std::expected instances and how to access its value or error. Then, we went through a couple of examples.

There’s still more to describe, so stay tuned for more articles, for example, about refactoring and monadic extensions.

See the next one about internals of std::expected

Back to you

  • Have you played with std::expected?
  • Have you worked with similar types in other languages or libraries?

Share your comments below.

Notes