Table of Contents

std::expected from C++23 not only serves as an error-handling mechanism but also introduces functional programming paradigms into the language. In this blog post, we’ll have a look at functional/monadic extensions of std::expected, which allow us to chain operations elegantly, handling errors at the same time. The techniques are very similar to std::optional extensions - see How to Use Monadic Operations for `std::optional` in C++23 - C++ Stories.

Here’s a brief overview of these functional capabilities:

Update: Thanks to prof. Boguslaw Cyganek for finding some errors in my initial text!

and_then()  

The and_then member function enables chaining operations that might produce a std::expected object. It’s invoked when the std::expected object holds a value and allows for seamless operation chaining without manual error checking after each step.

See below a simple example:

#include <iostream>
#include <expected>

std::expected<int, std::string> incrementIfPositive(int value) {
    if (value > 0) 
        return value + 1;
    return std::unexpected("Value must be positive");
}

std::expected<int, std::string> getInput(int x) {
    if (x % 2 == 0)
        return x;
    
    return std::unexpected("Value not even!");
}

int main() {
    auto input = getInput(-2);
    auto result = input.and_then(incrementIfPositive);

    if (result)
        std::cout << *result << '\n';
    else
        std::cout << result.error() << '\n';
}

See at @Compiler Explorer

The getInput function returns an expected object with the value of -2; since this is not an error, it’s passed to incrementIfPositive.

Please notice that incrementIfPositive takes the type of the contained value of the starting object, but returns std::expected<T, Err> type.

And here’s the output:

Value must be positive

And for the comparison, let’s have a look at a more complicated example. Here’s a version with regular "if/else" checking:

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

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

std::expected<int, std::string> calculate(int number) {
    if (number > 0)
        return number * 2;
    
    return std::unexpected("Calculation failed: number must be positive");
}

int main() {
    std::string input;
    //std::cout << "Enter a number: ";
    //std::cin >> input;
    input = "123";

    auto converted = convertToInt(input);
    if (!converted) {
        std::cout << converted.error() << '\n';
        return -1;
    }

    auto result = calculate(*converted);
    if (!result) {
        std::cout << result.error() << '\n';
        return -1;
    }

    std::cout << "Result: " << *result << '\n';
}

Run @Compiler Explorer

Here’s the version with and_then():

// Same convertToInt function as above...
// Same calculate function as above...

int main() {
    std::string input;
    //std::cout << "Enter a number: ";
    //std::cin >> input;
    input = "123";

    auto result = convertToInt(input)
                    .and_then(calculate)
                    .and_then([](int num) {
                        std::cout << "Result: " << num << '\n';
                        return std::expected<int, std::string>(num);
                    });

    if (!result)
        std::cout << result.error() << '\n';
}

See @Compiler Explorer

Notice that the latter version omits one check for the input expected object. In the example, I also mixed a regular function pointer with a lambda.

transform() - Transforming the Value  

The transform function applies a given function to the contained value if the std::expected object holds a value. It allows for transforming the value without altering the error state.

double doubleValue(int value) {
    return value * 2.;
}

auto tr = std::expected<int, Err>(10).transform(doubleValue);
// ts now is std::expected<double, Err>

Run at Compiler Explorer

Please notice that doubleValue takes the type of the contained value of the starting object, and returns the same or other Value type. The returned value is wrapped into expected type.

And here’s a better example, where we transform the parsed date:

#include <iostream>
#include <string>
#include <sstream>
#include <expected>
#include <chrono>
#include <format>

std::expected<std::chrono::year_month_day, std::string> 
parseDate(const std::string& dateStr) {
    std::istringstream iss(dateStr);
    std::chrono::year_month_day ymd;
    iss >> std::chrono::parse("%F", ymd);
    if (!ymd.ok())
        return std::unexpected("Parsing failed: invalid date format");

    return ymd;
}

std::string formatDate(const std::chrono::year_month_day& ymd) {
    return std::format("Back to string: {:%Y-%m-%d}", ymd);
}

int main() {
    std::string input = "2024-04-29";
    auto result = parseDate(input)
                    .transform(formatDate); 

    if (!result) {
        std::cout << "Error: " << result.error() << '\n';
        return -1;
    }

    std::cout << "Formatted Date: " << *result << '\n';
}

Run @Compiler Explorer

or_else() - Handling Errors  

or_else handles errors by applying a function to the contained error if the std::expected object holds an error. It’s useful for logging, error transformation, or recovery strategies.

From cppreference:

If this contains an unexpected value, invokes f with the argument error() and returns its result; otherwise, returns a std::expected object that contains a copy of the contained expected value (obtained from operator).

std::expected<int, std::string> handleError(const std::string& error) {
    std::cerr << "Error encountered: " << error << '\n';
    return std::unexpected(error);
}

auto handled = std::expected<int, std::string>(std::unexpected("Failure")).or_else(handleError);

or_else works great with and_then:

std::expected<int, std::string> handleError(const std::string& error) {
    std::cerr << "Error encountered: " << error << '\n';
    return std::unexpected(error);
}

int main() {
    auto input = getInput(-2);
    auto result = input.and_then(incrementIfPositive)
                       .or_else(handleError);
}

Run at @Compiler Explorer

The output:

Error encountered: Value must be positive

Notice that our error handler code can be invoked in two cases: when the input is in the error state or the returned value from incrementIfPositive is in the error state.

transform_error()  

This is similar to transform() but applies to the error part. It allows transforming the error into another form without affecting the success path.

std::string transformError(const std::string& error) {
    return std::ustring("New Error: ") + error;
}

auto errorTransformed = std::expected<int, std::string>(std::unexpected("Original Error"))
                          .transform_error(transformError);

Run @Compiler Explorer

Please notice that transformError takes the type of the contained error of the starting object, and returns the same or other Error type. The returned error is wrapped into expected type.

Using them all together:  

Here’s a more complicated example that uses all of that functionality:

#include <iostream>
#include <string>
#include <sstream>
#include <expected>
#include <chrono>
#include <format>

bool isWeekend(const std::chrono::year_month_day& ymd) {
    auto wd = std::chrono::weekday(ymd);
    return wd == std::chrono::Sunday || wd == std::chrono::Saturday;
}

std::expected<std::chrono::year_month_day, std::string> parseDate(const std::string& dateStr) {
    std::istringstream iss(dateStr);
    std::chrono::year_month_day ymd;
    iss >> std::chrono::parse("%F", ymd);
    if (!ymd.ok())
        return std::unexpected("Parsing failed: invalid date format");
    return ymd;
}

std::string formatDate(const std::chrono::year_month_day& ymd) {
    return std::format("{:%Y-%m-%d}", ymd);
}

int main() {
    std::string input = "2024-04-28";  // This is a Saturday
    auto result = parseDate(input)
        .and_then([](const std::chrono::year_month_day& ymd) 
                  -> std::expected<std::chrono::year_month_day, std::string> {
            if (isWeekend(ymd)) {
                return std::unexpected("Date falls on a weekend");
            }
            return ymd; 
        })
        .transform([](const std::chrono::year_month_day& ymd) -> std::string {
            return formatDate(ymd);
        })
        .transform_error([](const std::string& error) -> std::string {
            return "Error encountered: " + error;
        })
        .or_else([](const std::string& error) {
            std::cout << "Handled Error: " << error << '\n';
            return std::expected<std::string, std::string>(std::unexpected(error));
        });

    if (result)
        std::cout << "Formatted Date: " << *result << '\n';
    else
        std::cout << "Final Error: " << result.error() << '\n';
}

Run @Compiler Explorer

  • Check Weekend and Pass Date: Using and_then, the date is checked if it’s a weekend. If not, it passes the date forward.
  • Format Date with transform: The transform function is applied only if the previous and_then succeeds. It converts the std::chrono::year_month_day to a formatted string.
  • Customize Error Message with transform_error: If any error occurs (either parsing error or weekend error), the transform_error function modifies the error message to add additional context or change its format.
  • Final Error Handling with or_else: Finally, or_else is used to handle or log any resulting error after transformations. This step ensures that any error, transformed or not, is addressed appropriately and could also include logic to recover or default if desired.

Summary  

In this text, we explored some handy ways of working with the std::expected type. Thanks to monadic extensions, you can wrap the logic into a chain of callbacks/lambdas and avoid duplicate error checking.

This is a new style and requires some training, but it’s a very interesting and promising option.

Also, have a look at my previous article on optional: How to Use Monadic Operations for std::optional in C++23 - C++ Stories.

But there’s more! While the text showed a couple of examples, in the next blog post, we’ll have a look at some existing code and see where std::expected is used in real projects.

Back to you

  • Have you tried monadic extensions for std::expected or std::optional?
  • Do you prefer that new style or the regular “if/else” approach?

Share your comments below.