C++17 implementation of std::expected
conforming to the interface proposed in P0323R9.
The current standard is somewhat limiting when it comes to error handling. We are forced to either juggle error codes (efficient but messy) or rely on exceptions (expressive but inefficient). Languages such as Haskell and Rust have instead chosen to use monadic error handling. This way, functions that may fail can return an instance of one of two types, one being of the 'correct' return type and one being of an error type. std::expected
is this approach brought to C++. This would allow for the expressiveness of exceptions but with a significantly smaller performance hit (a single boolean check is enough), while also making the code more concise.
Using expected
, one might replace the following
extern std::vector<int> function_that_may_fail();
void foo() {
std::vector<int> vec;
try {
vec = function_that_may_fail();
}
catch(std::exception& e) {
std::cerr << e.what() << "\n";
/* Additional error handling */
return;
}
/* Do something with vec */
}
with
extern vien::expected<std::vector<int>, std::string> function_that_may_fail();
void foo() {
auto e = function_that_may_fail();
if(e.has_value()) {
/* Do something with e.value() */
}
else {
std::cerr << e.error() << "\n";
/* Additional error handling */
}
}
In addition to what is proposed in P0323R9, the implementation provides a few functional extensions, inspired in part by Rust's Result enum. In order to adhere to the interface proposed for the standard, these are opt-in and require that VIEN_EXPECTED_EXTENDED
is defined before the expected header is included. The following extensions are available:
-
map
invokes a callable on the contained value, leaving a potential unexpected unchanged.vien::expected<int, double> e1(10); // bool(e1) == true vien::expected<int, double> e1 = e1.map([](int i) { return 2 * i; }); ASSERT(*e1 == 20);
-
map_range
likemap
but applies the callable to each element in a contained container.std::string str = "expected" vien::expected<std::string, int> e1(std::move(str)); // bool(e1) == true vien::expected<std::string, int> e2 = e1.map_range([](unsigned char c) { return std::toupper(c); }); ASSERT(*e2 == "EXPECTED");
-
map_error
invokes a callable on the contained unexpected, leaving a potential value unchanged.vien::expected<int, std::string> e1(unexpect, "error"); // bool(e1) == false vien::expected<int, std::string> e2 = e1.map_error([](auto const& str) { return "fatal " + str; }); ASSERT(e2.error() == "fatal error");
-
map_or_else
invokes a callable on the contained value, if any. Otherwise, invokes the fallback callable on the contained unexpected.auto multiply_by_two = [](int i) { return 2 * i; }; auto flip_sign = [](int i) { return -i; }; vien::expected<int, int> e1(5); // bool(e1) == true vien::expected<int, int> e2(unexpect, 20); // bool(e2) == false vien::expected<int, int> e3 = e1.map_or_else(multiply_by_two, flip_sign); vien::expected<int, int> e4 = e2.map_or_else(multiply_by_two, flip_sign); ASSERT(e3.value() == 10); ASSERT(e4.error() == -20);
-
and_then
invokes a callable if the expected has a value. If the expected holds an unexpected, nothing is done.auto square = [](int i) { return i * i; }; vien::expected<int, std::string> e1(2); // bool(e1) == true vien::expected<int, std::string> e2 = e1.and_then(square).and_then(square); ASSERT(*e2 == 8);
-
or_else
invokes a callable if the expected has no value. If the expected holds a value, nothing is done.auto square = [](int i) { return i * i; }; vien::expected<std::string, int> e1(unexpect, 2); // bool(e1) == false vien::expected<std::string, int> e2 = e1.or_else(square).or_else(square); ASSERT(e2.error() == 8);
Confirmed working on GCC, Clang, MSVC and Cygwin.
Catch2 is used for testing. The single-header version is included in the tests directory.
Simon Brand has written a very well received implementation with support ranging back to C++11.