3938. Cannot use std::expected monadic ops with move-only error_type

Section: 22.8.6.7 [expected.object.monadic] Status: WP Submitter: Jonathan Wakely Opened: 2023-05-25 Last modified: 2023-11-22

Priority: Not Prioritized

View all other issues in [expected.object.monadic].

View all issues with WP status.

Discussion:

The monadic ops for std::expected are specified in terms of calls to value() and error(), but LWG 3843 ("std::expected<T,E>::value()& assumes E is copy constructible") added additional Mandates requirements to value(). This means that you can never call value() for a move-only error_type, even the overloads of value() with rvalue ref-qualifiers.

The changes to value() are because it needs to be able to throw a bad_expected_access<E> which requires a copyable E. But in the monadic ops we know it can't throw, because we always check. All the monadic ops are of the form:

if (has_value())
  do something with value();
else
  do something with error();

We know that value() won't throw here, but because we use "Effects: Equivalent to ..." the requirement for E to be copyable is inherited from value().

Should we have changed the monadic ops to use operator*() instead of value()? For example, for the first and_then overloads the change would be:

-4- Effects: Equivalent to:
if (has_value())
  return invoke(std::forward<F>(f), value()**this);
else
  return U(unexpect, error());

[2023-06-01; Reflector poll]

Set status to Tentatively Ready after seven votes in favour during reflector poll.

[2023-06-17 Approved at June 2023 meeting in Varna. Status changed: Voting → WP.]

Proposed resolution:

This wording is relative to N4950.

  1. For each Effects: element in 22.8.6.7 [expected.object.monadic], replace value() with **this as indicated:

    
    template<class F> constexpr auto and_then(F&& f) &;
    template<class F> constexpr auto and_then(F&& f) const &;
    

    -1- Let U be remove_cvref_t<invoke_result_t<F, decltype(value()**this)>>.

    -2- Constraints: is_constructible_v<E, decltype(error())> is true.

    -3- Mandates: U is a specialization of expected and is_same_v<U::error_type, E> is true.

    -4- Effects: Equivalent to:

    if (has_value())
      return invoke(std::forward<F>(f), value()**this);
    else
      return U(unexpect, error());
    
    
    template<class F> constexpr auto and_then(F&& f) &&;
    template<class F> constexpr auto and_then(F&& f) const &&;
    

    -5- Let U be remove_cvref_t<invoke_result_t<F, decltype(std::move(value()**this))>>.

    -6- Constraints: is_constructible_v<E, decltype(std::move(error()))> is true.

    -7- Mandates: U is a specialization of expected and is_same_v<U::error_type, E> is true.

    -8- Effects: Equivalent to:

    if (has_value())
      return invoke(std::forward<F>(f), std::move(value()**this));
    else
      return U(unexpect, std::move(error()));
    
    
    template<class F> constexpr auto or_else(F&& f) &;
    template<class F> constexpr auto or_else(F&& f) const &;
    

    -9- Let G be remove_cvref_t<invoke_result_t<F, decltype(error())>>.

    -10- Constraints: is_constructible_v<T, decltype(value()**this)> is true.

    -11- Mandates: G is a specialization of expected and is_same_v<G::value_type, T> is true.

    -12- Effects: Equivalent to:

    if (has_value())
      return G(in_place, value()**this);
    else
      return invoke(std::forward<F>(f), error());
    
    
    template<class F> constexpr auto or_else(F&& f) &&;
    template<class F> constexpr auto or_else(F&& f) const &&;
    

    -13- Let G be remove_cvref_t<invoke_result_t<F, decltype(std::move(error()))>>.

    -14- Constraints: is_constructible_v<T, decltype(std::move(value()**this))> is true.

    -15- Mandates: G is a specialization of expected and is_same_v<G::value_type, T> is true.

    -16- Effects: Equivalent to:

    if (has_value())
      return G(in_place, std::move(value()**this));
    else
      return invoke(std::forward<F>(f), std::move(error()));
    
    
    template<class F> constexpr auto transform(F&& f) &;
    template<class F> constexpr auto transform(F&& f) const &;
    

    -17- Let U be remove_cv_t<invoke_result_t<F, decltype(value()**this)>>.

    -18- Constraints: is_constructible_v<E, decltype(error())> is true.

    -19- Mandates: U is a valid value type for expected. If is_void_v<U> is false, the declaration

      U u(invoke(std::forward<F>(f), value()**this));
    is well-formed.

    -20- Effects:

    1. (20.1) — If has_value() is false, returns expected<U, E>(unexpect, error()).
    2. (20.2) — Otherwise, if is_void_v<U> is false, returns an expected<U, E> object whose has_val member is true and val member is direct-non-list-initialized with invoke(std::forward<F>(f), value()**this).
    3. (20.3) — Otherwise, evaluates invoke(std::forward<F>(f), value()**this) and then returns expected<U, E>().

    
    template<class F> constexpr auto transform(F&& f) &&;
    template<class F> constexpr auto transform(F&& f) const &&;
    

    -21- Let U be remove_cv_t<invoke_result_t<F, decltype(std::move(value()**this))>>.

    -22- Constraints: is_constructible_v<E, decltype(std::move(error()))> is true.

    -23- Mandates: U is a valid value type for expected. If is_void_v<U> is false, the declaration

      U u(invoke(std::forward<F>(f), std::move(value()**this)));
    is well-formed for some invented variable u.

    [Drafting Note: The removal of "for some invented variable u" in paragraph 23 is a drive-by fix for consistency with paragraphs 19, 27 and 31.]

    -24- Effects:

    1. (24.1) — If has_value() is false, returns expected<U, E>(unexpect, error()).
    2. (24.2) — Otherwise, if is_void_v<U> is false, returns an expected<U, E> object whose has_val member is true and val member is direct-non-list-initialized with invoke(std::forward<F>(f), std::move(value()**this)).
    3. (24.3) — Otherwise, evaluates invoke(std::forward<F>(f), std::move(value()**this)) and then returns expected<U, E>().

    
    template<class F> constexpr auto transform_error(F&& f) &;
    template<class F> constexpr auto transform_error(F&& f) const &;
    

    -25- Let G be remove_cv_t<invoke_result_t<F, decltype(error())>>.

    -26- Constraints: is_constructible_v<T, decltype(value()**this)> is true.

    -27- Mandates: G is a valie template argument for unexpected ( [unexpected.un.general]) and the declaration

      G g(invoke(std::forward<F>(f), error()));
    is well-formed.

    -28- Returns: If has_value() is true, expected<T, G>(in_place, value()**this);; otherwise, an expected<T, G> object whose has_val member is false and unex member is direct-non-list-initialized with invoke(std::forward<F>(f), error()).

    
    template<class F> constexpr auto transform_error(F&& f) &&;
    template<class F> constexpr auto transform_error(F&& f) const &&;
    

    -29- Let G be remove_cv_t<invoke_result_t<F, decltype(std::move(error()))>>.

    -30- Constraints: is_constructible_v<T, decltype(std::move(value()**this))> is true.

    -31- Mandates: G is a valie template argument for unexpected ( [unexpected.un.general]) and the declaration

      G g(invoke(std::forward<F>(f), std::move(error())));
    is well-formed.

    -32- Returns: If has_value() is true, expected<T, G>(in_place, std::move(value()**this));; otherwise, an expected<T, G> object whose has_val member is false and unex member is direct-non-list-initialized with invoke(std::forward<F>(f), std::move(error())).