How to bubble up `std::unexpected` values when the expected types are different?

1 day ago 6
ARTICLE AD BOX

This question is very similar to "How can we forward a failed std::expected up through the call stack". Given a chain of function calls that each return std::expected values, errors should percolate back up the chain.

The wrinkle here is that, although the error type is the same at all level, the expected types vary. As far as I can tell, that means you can't use the monadic operations to streamline the code.

Consider this toy recursive descent parser:

#include <cstdlib> #include <expected> #include <iostream> #include <string> #include <utility> struct Error { std::string msg; }; struct Bar { int x; }; struct Foo { Bar pair[2]; }; std::expected<Bar, Error> ParseBar() { return std::unexpected<Error>("syntax error in Bar"); } std::expected<Foo, Error> ParseFoo() { auto bar0 = ParseBar(); if (!bar0.has_value()) return std::unexpected{std::move(bar0.error())}; auto bar1 = ParseBar(); if (!bar1.has_value()) return std::unexpected{std::move(bar1.error())}; Foo foo; foo.pair[0] = std::move(bar0.value()); foo.pair[1] = std::move(bar1.value()); return foo; } int main() { auto foo = ParseFoo(); if (!foo.has_value()) { std::cerr << foo.error().msg; std::exit(EXIT_FAILURE); } return 0; }

I find this part of the syntax a bit unwieldy:

return std::unexpected{std::move(bar0.error())}

The boilerplate code visually buries the bar0.error(), which is the key to understanding the intent. (A helper function can help, but I don't consider it a great solution.)

Am I missing something that can be done with monadic operations?

Read Entire Article