Last Update:
std::expected - Monadic Extensions
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 thatincrementIfPositive
takes the type of the contained value of the starting object, but returnsstd::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';
}
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';
}
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 thatdoubleValue
takes the type of the contained value of the starting object, and returns the same or other Value type. The returned value is wrapped intoexpected
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';
}
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);
Please notice thattransformError
takes the type of the contained error of the starting object, and returns the same or other Error type. The returned error is wrapped intoexpected
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';
}
- 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
: Thetransform
function is applied only if the previousand_then
succeeds. It converts thestd::chrono::year_month_day
to a formatted string. - Customize Error Message with
transform_error
: If any error occurs (either parsing error or weekend error), thetransform_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.
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here: