Title
Monadic operations of `optional` and `expected` drop the cv-qualification of `T`
Status
new
Section
[optional.monadic] [expected.object.monadic]
Submitter
Tianjiao Yin

Created on 2026-11-10.00:00:00 last changed yesterday

Messages

Date: 2026-11-14.16:42:20

Proposed resolution:

This wording is relative to N5054.

  1. Modify [optional.monadic] 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 invoke_result_t<F, decltype((val)operator*())>.

    -2- Mandates: remove_cvref_t<U> is a specialization of `optional`.

    -3- Effects: Equivalent to:

    if (*this) {
      return invoke(std::forward<F>(f), valoperator*());
    } else {
      return remove_cvref_t<U>();
    }
    
    template<class F> constexpr auto and_then(F&& f) &&;
    template<class F> constexpr auto and_then(F&& f) const &&;
    

    -4- Let `U` be invoke_result_t<F, decltype(std::move(valoperator*()))>.

    -5- Mandates: remove_cvref_t<U> is a specialization of `optional`.

    -6- Effects: Equivalent to:

    if (*this) {
      return invoke(std::forward<F>(f), std::move(valoperator*()));
    } else {
      return remove_cvref_t<U>();
    }
    
    template<class F> constexpr auto transform(F&& f) &;
    template<class F> constexpr auto transform(F&& f) const &;
    

    -7- Let `U` be remove_cv_t<invoke_result_t<F, decltype((val)operator*())>>.

    -8- Mandates: `U` is a valid contained type for `optional`. The declaration

    U u(invoke(std::forward<F>(f), valoperator*()));
    

    is well-formed for some invented variable `u`.

    [Note 1: There is no requirement that `U` is movable ([dcl.init.general]). — end note]

    -9- Returns: If `*this` contains a value, an optional<U> object whose contained value is direct-non-list-initialized with invoke(std::forward<F>(f), valoperator*()); otherwise, optional<U>().

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

    -10- Let `U` be remove_cv_t<invoke_result_t<F, decltype(std::move(valoperator*()))>>.

    -11- Mandates: `U` is a valid contained type for `optional`. The declaration

    U u(invoke(std::forward<F>(f), std::move(valoperator*())));
    

    is well-formed for some invented variable `u`.

    [Note 2: There is no requirement that U is movable ([dcl.init.general]). — end note]

    -12- Returns: If `*this` contains a value, an optional<U> object whose contained value is direct-non-list- initialized with invoke(std::forward<F>(f), std::move(valoperator*())); otherwise, optional<U>().

  2. Modify [expected.object.monadic] 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((val)operator*())>>.

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

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

    -4- Effects: Equivalent to:

    if (has_value())
      return invoke(std::forward<F>(f), valoperator*());
    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(valoperator*()))>>.

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

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

    -8- Effects: Equivalent to:

    if (has_value())
      return invoke(std::forward<F>(f), std::move(valoperator*()));
    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((val)operator*())> is `true`.

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

    -12- Effects: Equivalent to:

    if (has_value())
      return G(in_place, valoperator*());
    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(valoperator*()))> is `true`.

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

    -16- Effects: Equivalent to:

    if (has_value())
      return G(in_place, std::move(valoperator*()));
    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((val)operator*())>>.

    -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), valoperator*()));
    

    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), valoperator*()).

    3. (20.3) — Otherwise, evaluates invoke(std::forward<F>(f), valoperator*()) 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(valoperator*()))>>.

    -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(valoperator*())));
    

    is well-formed.

    -24- Effects:

    1. (24.1) — If `has_value()` is `false`, returns expected<U, E>(unexpect, std::move(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(valoperator*())).

    3. (24.3) — Otherwise, evaluates invoke(std::forward<F>(f), std::move(valoperator*())) 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((val)operator*())> is `true`.

    -27- Mandates: `G` is a valid template argument for `unexpected` ([expected.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, valoperator*()); 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(valoperator*()))> is `true`.

    -31- Mandates: `G` is a valid template argument for `unexpected` ([expected.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(valoperator*())); 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())).

Date: 2026-11-10.00:00:00

The resolution of LWG 3973 (adopted in Kona) changed all occurrences of `value()` in [optional.monadic] to *val, and of `**this` in [expected.object.monadic] to val. The intention was not to change the meaning, just avoid the non-freestanding `value()` function, and avoid ADL that would be caused by using `**this`.

LWG 4015 then changed the exposition-only member of `optional` to

union { remove_cv_t<T> val; };

following LWG 3891 for `expected`, and changed *val to val.

However, because val drops the cv-qualifiers of `T`, in the non-const overloads such as and_then(F&&) & the type of val is remove_cv_t<T>&, whereas the type of `value()` was T&. This means the callable can modify a const contained value:

std::optional<const int> o(1);
o.and_then([](int& r) { r = 2; return std::optional<int>(r); }); // *o is now 2

std::expected<const int, int> e(1);
e.transform([](int& r) { return r = 2; }); // *e is now 2

This was ill-formed in C++23. The rvalue overloads have the same problem (std::move(val) is remove_cv_t<T>&& rather than T&&), as does volatile T.

Using `operator*()` instead of val restores the type that `value()` had, is freestanding, and does not perform ADL.

History
Date User Action Args
2026-11-14 16:42:20adminsetmessages: + msg16654
2026-11-10 00:00:00admincreate