This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

4642. Monadic operations of optional and expected drop the cv-qualification of T

Section: 22.5.3.8 [optional.monadic], 22.8.6.7 [expected.object.monadic] Status: New Submitter: Tianjiao Yin Opened: 2026-11-10 Last modified: 2026-11-14

Priority: Not Prioritized

View other active issues in [optional.monadic].

View all other issues in [optional.monadic].

View all issues with New status.

Discussion:

The resolution of LWG 3973(i) (adopted in Kona) changed all occurrences of value() in 22.5.3.8 [optional.monadic] to *val, and of **this in 22.8.6.7 [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(i) then changed the exposition-only member of optional to

union { remove_cv_t<T> val; };

following LWG 3891(i) 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.

Proposed resolution:

This wording is relative to N5054.

  1. Modify 22.5.3.8 [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 (9.5.1 [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 (9.5.1 [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 22.8.6.7 [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 (22.8.3.1 [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 (22.8.3.1 [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())).