C++ Logo

std-proposals

Advanced search

[std-proposals] Suggeting improvements to std::expected<T, std::exception_ptr>

From: Stewart Becker <stewart_at_[hidden]>
Date: Sun, 30 Mar 2025 20:41:07 +0100
I have been doing work in the world of monads and functional
programming, and have found std::expected to be a key building block for
such work. It works particularly well when the error type is the
catch-all std::exception_ptr. However, there are still some drawbacks.
What would be even better would be if:

 1. std::exception_ptr supported references (including rvalue
    references) for the value type,
 2. std::exception_ptr::value() called std::rethrow_exception(error())
    rather than throwing a std::bad_expected_access on error, and
 3. std::exception_ptr<T, std::exception_ptr>::transform(f) and
    ::and_then(f) were noexcept. Completely noexcept is probably
    impossible, but it should be possible to wrap the transformation
    itself in try/catch, only throwing if the final copy/move of the
    returned result does so.

To the first point, I have found that, in order to work effectively with
monadic continuations, more than return-by-value is needed. While a
reference can be wrapped in a std::reference_wrapper or a pointer, this
doesn't allow for generic programming. There also are no vocabulary
wrappers that distinguish between lvalue and rvalue references.

The paper P2988 (std::optional<T&>) expected<T&> in passing and makes
the case for std::expected<T&, E>, but only actually proposes matters
for std::optional<T&>, noting "... we expect future papers to propose
std::expected<T&, E> ...". Well, maybe it is time for such a paper.

Secondly, the purpose of std::expected is to encpasulate error handling,
but when exp.transform(f) and exp.and_then(f) throw, this encapsulation
is broken. Additionally, I find that throwing std::bad_expected_access
is a leaky abstraction - albeit one necessary for other error types. In
each case, it can be worked around by wrapping calls to the expected in
a try / catch and either rewrapping (.transform) or extracting and
rethrowing (.value) the caught exception. However, this adds a lot of
boilerplate and significantly reduces the advantages of using
std::extepected in the first place! This could all be covered within
std::expected<T, std::exception_ptr> itself.

Paper P3014 (Customising std::expected's exception) touches on some of
the above, but I don't think in its current form would accommodate
calling rethrow_exception when E is std::exception_ptr. While P3014
states that "User code is allowed to customizestd::expected_traitsfor
their own error types", std::exception_ptr is not a user type.

I do acknowledge that for std::expected<T, std::exception_ptr>::value to
call std::rethrow_exception would be a change of behaviour. However,
one way to square this circle could be if std::expected took a third,
defaulted, type parameter, taking the role of P3014's
expected_traits<E>. Current behaviour can be retained by the default
but still allow for customisation. I don't know if there is precedent
for adding a type argument to an existing library class template. While
this too would be a breaking change inasmuch as adding an addition
template paremeter means std::expected would no longer match
template<template<typename, typename> class Exp>, it is perhaps the
lesser of two evils. Beyond that, it is difficult to see how this would
break existing code. I have reached out to Jonathan Muller (P3014's
author), but received no response.

I don't know of any work on noexcept versions of
std::expected::transform or std::expected::and_then. These are perhaps
the simplest to achieve. std::expected<T, std::exception_ptr> could be
specialised to have additional noexcept member functions try_transform
and try_and_then, which internally call transform(f) and and_then(f)
within a try/catch block.

I would hope that adding support for reference types to std::expected is
uncontroversial, particularly in the light of P2988. The other aspects
may be more contentious. It would be possible to design an entirely new
std::expected-like class template with those features. However,
std::expected delivers a lot of valuable functionality that would simply
be duplicated; do the additional features above really warrant a new
type rather than attempting to improve std::expected? We already have
at least three patterns for indicating errors, (returning error codes,
throwing exceptions and encapsulating in std::expected). Adding yet
another - especially one so similar to an existing pattern - would seem
to complicate the space for little benefit. On the other hand, if the
type could implicitly convert to and from std::expected, it may be a
viable way forward.

Stewart Becker



Received on 2025-03-30 19:41:11