10

I modeled a function that returns an instance of an std::expected<void, Error> - I told myself "I can use a new standard so I will design my library accordingly" - and I was very positive to have nice and concrete error handling. Now it turns out, that all the monadic operations on the std::expected only work on non-void items. Even though there is a specialization for void, the monadic operations are not available. I understand that or_else needs to return the value - but there is a specialization for void, so why should this not work?

std::expected<void, Error> fun (int);
fun (19).and_then ([]() { doSomething(); }).or_else ([] (Error e) { std::println ("Uh oh error.."); });

This yields:

/usr/include/c++/14.2.1/expected:1586:37: error: static assertion failed
 1586 | static_assert(__expected::__is_expected<_Up>);
 | ~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
/usr/include/c++/14.2.1/expected:1586:37: note: ‘std::__expected::__is_expected<void>’ evaluates to false
/usr/include/c++/14.2.1/expected:1587:25: error: ‘std::remove_cvref<void>::type’ {aka ‘void’} is not a class, struct, or union type
 1587 | static_assert(is_same_v<typename _Up::error_type, _Er>);
 | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14.2.1/expected:1592:20: error: expression list treated as compound expression in functional cast [-fpermissive]
 1592 | return _Up(unexpect, std::move(_M_unex));
 | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/david/tests/cpp/src/expect.cpp: In function ‘int main()’:
/home/david/tests/cpp/src/expect.cpp:49:26: error: invalid use of ‘void’

What's the problem here? Is this feature only half done (so the standard is incomplete) or am I misunderstanding its point?

asked Nov 19, 2024 at 9:04
3
  • 3
    The problem is that the callable to and_then and or_else needs to return a specialization of std::expected for chaining to work. Your lambdas do not. Commented Nov 19, 2024 at 9:10
  • Thanks for the hint - since the expected value is of type void - what should the callable to and_then return? Commented Nov 19, 2024 at 9:27
  • I expanded my comment into an answer. Commented Nov 19, 2024 at 9:34

2 Answers 2

17

The problem is that the callable to and_then and or_else needs to return a specialization of std::expected for chaining to work. Your lambdas do not.

If fun were to return an unexpected (an error), and_then won't call your callable, but it will need to forward the error into the same sort of unexpected type that the lambda returns. So the lambda for and_then should return std::unexpected<..., Error>. You can choose what ever "expected" type you want the lambda to return, just as long as it's the same Error type. You can stick to void of course.

fun (19).and_then ([]() { doSomething(); return std::expected<void, Error>(); })

Conversly, the callbale to or_else can choose a different error type, but needs to preserve the expected type in its returned std::expected specialization. But all in all, the same treatment should work.

fun (19).and_then ([]() { 
 doSomething();
 return std::expected<void, Error>();
}).or_else ([] (Error e) { 
 std::println ("Uh oh error..");
 return std::expected<void, Error>();
});

Note I opted to have or_else swallow the error and return an std::expected with a value. You could choose to forward the error, of course.

...
.or_else ([] (Error e) { 
 std::println ("Uh oh error..");
 return std::expected<void, Error>(std::unexpect, e);
});
answered Nov 19, 2024 at 9:33
Sign up to request clarification or add additional context in comments.

5 Comments

Thank you for your answer! I didn't expect this! I assumed and_then and or_else do some lifting for me, especially with expected<void this is fairly useless (the member functions could take care about returning the expected value - they have it at hand already) and creates a lot of boilerplate. I need to rethink my design.
@David - I don't recall exactly why the design of the monadic operations landed on this. But I believe it's down to letting and_then report errors as well that may then flow into or_else. It seems useful anyway, despite the boiler-plate.
On Rust's Result, there's two operations: map for which the user provides a fn(T)->U function, which requires the callback to be infallible thus, and and_then for which the user provides a fn(T)->Result<U, E> function, which allows the callback to fail on its own. It could be that std::unexpected simply followed precedent here, given the names and semantics of the monadic operations match?
And I just noticed that @attempt0 mentioned that Result map and map_err are called transform and transform_error on std::expected, so we indeed have full parity.
@MatthieuM. - I followed the paper trail, and Rust's Result is unsurprisingly mentioned wg21.link/p2505 - No coincidence there
13

Although this is already answered by another answer to this question, let me add that transform and transform_error is probably what you expected for the library function instead.

fun(19).transform([]() { doSomething(); }).transform_error([](Error e) {
 std::println("Uh oh error..");
 return e;
});

There is a difference between monad and functor.

  • and_then and or_else behaves monadically. and_then take in a function T -> std::expected<U, E> and transform from std::expected<T, E> to std::expected<U, E>
  • transform and transform_error behave like functor. transform take in a function T -> U and transform from std::expected<T, E> to std::expected<U, E>.

It is interesting that the transform_error cannot have U = void if T is not void based on my testing. The monadic variant is more expressive as you can be returning a different result, for example returning the unexpected variant whenever something goes wrong inside and_then instead.

chwarr
7,3702 gold badges34 silver badges58 bronze badges
answered Nov 19, 2024 at 10:21

2 Comments

Great summary. Though IIUC U = void should work regardless (just not for an error).
Thank you! I just got a whole lot of learning about monads. Monoids, Monads, and Applicative Functors: Repeated Software Patterns by David Sankel at CppCon 2020

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.