Title
`optional::value_or` return statement is inconsistent with Mandates
Status
new
Section
[optional.observe] [optional.ref.observe] [expected.object.obs]
Submitter
Hewill Kang

Created on 2025-09-06.00:00:00 last changed 2 weeks ago

Messages

Date: 2025-11-19.18:55:35

Proposed resolution:

This wording is relative to the working draft after N5014.

  1. Modify [optional.optional.general] as indicated:

    […]
    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&&) const &;
    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&&) &&;
    […]
    
  2. Modify [optional.observe] as indicated:

    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&& v) const &;
    

    -?- Let X be remove_cv_t<T>.

    -15- Mandates: is_copy_constructible_v<T> is_convertible_v<const T&, X> && is_convertible_v<U&&, T X> is `true`.

    -16- Effects: Equivalent to:

    return has_value() ? val : static_cast<T>(std::forward<U>(v));
    if (has_value())
      return val;
    return std::forward<U>(v);
    
    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&& v) &&;
    

    -?- Let X be remove_cv_t<T>.

    -17- Mandates: is_move_constructible_v<T> is_convertible_v<T, X> && is_convertible_v<U&&, T X> is `true`.

    -18- Effects: Equivalent to:

    return has_value() ? std::move(val) : static_cast<T>(std::forward<U>(v));
    if (has_value())
      return std::move(val);
    return std::forward<U>(v);
    
  3. Modify [optional.ref.observe] as indicated:

    constexpr T& value() const;
    

    -7- Effects: Equivalent to:

    return has_value() ? *val : throw bad_optional_access();
    if (has_value())
      return *val;
    throw bad_optional_access();
    
    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&& u) const;
    

    -8- Let X be remove_cv_t<T>.

    -9- Mandates: is_constructible_v<T> is_convertible_v<T&, X> && is_convertible_v<U, X> is `true`.

    -10- Effects: Equivalent to:

    return has_value() ? *val : static_cast<X>(std::forward<U>(u));
    if (has_value())
      return *val;
    return std::forward<U>(u);
    
  4. Modify [expected.object.general] as indicated:

    […]
    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&&) const &;
    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&&) &&;
    […]
    
  5. Modify [expected.object.obs] as indicated:

    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&& v) const &;
    

    -?- Let X be remove_cv_t<T>.

    -18- Mandates: is_copy_constructible_v<T> is `true` and is_convertible_v<const T&, X> && is_convertible_v<U, T> is `true`.

    -19- Returns: has_value() ? **this : static_cast<T>(std::forward<U>(v)).

    -?- Effects: Equivalent to:

    if (has_value())
      return val;
    return std::forward<U>(v);
    
    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&& v) &&;
    

    -?- Let X be remove_cv_t<T>.

    -20- Mandates: is_move_constructible_v<T> is `true` and is_convertible_v<T, X> && is_convertible_v<U, T> is `true`.

    -21- Returns: has_value() ? std::move(**this) : static_cast<T>(std::forward<U>(v)).

    -?- Effects: Equivalent to:

    if (has_value())
      return std::move(val);
    return std::forward<U>(v);
    
Date: 2025-11-15.00:00:00

[ 2025-11-19; Jonathan provides updated wording ]

Incorporate proposed resolution of 3424 as well, so that the return type is never cv-qualified.

Date: 2025-11-15.00:00:00

[ 2025-11-11; Jonathan provides updated wording ]

Rebase after LWG 4015 which was approved in Kona. This would also resolve LWG 4281.

Date: 2025-10-15.00:00:00

[ 2025-10-16; Reflector poll ]

Set priority to 3 after reflector poll.

This would need to use `val` instead of `**this` if LWG 4015 is accepted.

This wording is relative to N5014.

  1. Modify [optional.observe] as indicated:

    template<class U = remove_cv_t<T>> constexpr T value_or(U&& v) const &;
    

    -15- Mandates: is_copy_constructible_v<T> && is_convertible_v<U&&, T> is `true`.

    -16- Effects: Equivalent to:

    return has_value() ? **this : static_cast<T>(std::forward<U>(v));
    if (has_value())
      return **this;
    return std::forward<U>(v);
    
    template<class U = remove_cv_t<T>> constexpr T value_or(U&& v) &&;
    

    -17- Mandates: is_move_constructible_v<T> && is_convertible_v<U&&, T> is `true`.

    -18- Effects: Equivalent to:

    return has_value() ? std::move(**this) : static_cast<T>(std::forward<U>(v));
    if (has_value())
      return std::move(**this);
    return std::forward<U>(v);
    
  2. Modify [optional.ref.observe] as indicated:

    template<class U = remove_cv_t<T>> constexpr remove_cv_t<T> value_or(U&& u) const;
    

    -8- Let X be remove_cv_t<T>.

    -9- Mandates: is_constructible_v<X, T&> && is_convertible_v<U, X> is `true`.

    -10- Effects: Equivalent to:

    return has_value() ? *val : static_cast<X>(std::forward<U>(u));
    if (has_value())
      return *val;
    return std::forward<U>(u);
    
    
  3. Modify [expected.object.obs] as indicated:

    template<class U = remove_cv_t<T>> constexpr T value_or(U&& v) const &;
    

    -18- Mandates: is_copy_constructible_v<T> is `true` and is_convertible_v<U, T> is `true`.

    -19- Returns: has_value() ? **this : static_cast<T>(std::forward<U>(v)).

    -?- Effects: Equivalent to:

    if (has_value())
      return **this;
    return std::forward<U>(v);
    
    template<class U = remove_cv_t<T>> constexpr T value_or(U&& v) &&;
    

    -20- Mandates: is_move_constructible_v<T> is `true` and is_convertible_v<U, T> is `true`.

    -21- Returns: has_value() ? std::move(**this) : static_cast<T>(std::forward<U>(v)).

    -?- Effects: Equivalent to:

    if (has_value())
      return std::move(**this);
    return std::forward<U>(v);
    
Date: 2025-09-06.00:00:00

optional<T>::value_or(U&&) requires is_convertible_v<U&&, T> to ensure that `T` can be convert from `U` when `optional` has no value.

However, the return statement explicitly constructs `T` by `static_cast`, which is not checked by `is_convertible_v` since it only checks for implicit conversions.

This results in rare cases where Mandates may not be violated, but `value_or` is ill-formed (demo):

struct S {
  operator int() const;
  explicit operator int() = delete;
};

int main() {
   std::optional<int>{}.value_or(S{}); // fire
}

It is reasonable to create objects that stick to Mandates. The same goes for `expected::value_or`.

Daniel:

This issue has considerable overlap with LWG 4281.

History
Date User Action Args
2025-11-19 18:55:35adminsetmessages: + msg15755
2025-11-11 15:51:14adminsetmessages: + msg15733
2025-10-16 11:35:46adminsetmessages: + msg15183
2025-10-10 15:07:03adminsetmessages: + msg15134
2025-09-06 00:00:00admincreate