Title
LWG 3870 breaks std::expected<cv T, E>
Status
new
Section
[expected.object.general]
Submitter
Jiang An

Created on 2023-02-19.00:00:00 last changed 2 months ago

Messages

Date: 2024-10-02.11:44:43

Proposed resolution:

This wording is relative to N4988.

[Drafting note: When assignment and swap need to backup the old value by move construction, the source should be considered cv-unqualified, as the backup mechanism is only used internally.]

  1. Modify [expected.object.general] as indicated:

    […]
    bool has_val;      // exposition only
    union {
      remove_cv_t<T> val;       // exposition only
      E unex;          // exposition only
    };
    […]
    
  2. Modify [expected.object.assign] as indicated:

    constexpr expected& operator=(const expected& rhs);
    

    -2- Effects:

    1. (2.1) — If this->has_value() && rhs.has_value() is true, equivalent to value()val = *rhs.

    2. […]

    […]
    constexpr expected& operator=(expected&& rhs) noexcept(see below);
    

    […]

    -6- Effects:

    1. (6.1) — If this->has_value() && rhs.has_value() is true, equivalent to value()val = std::move(*rhs).

    2. […]

    […]
    template<class U = T>
      constexpr expected& operator=(U&& v);
    

    […]

    -10- Effects:

    1. (10.1) — If has_value() is true, equivalent to value()val = std::forward<U>(v).

    2. […]

  3. Modify Table 64: swap(expected&) effects [tab:expected.object.swap] as indicated:

    Table 64 — swap(expected&) effects [tab:expected.object.swap]
    this->has_value() !this->has_value()
    rhs.has_value() equivalent to: using std::swap;
    swap(value()val, rhs.value()val);
    calls rhs.swap(*this)
    […]
  4. Modify [expected.object.swap] as indicated:

    constexpr void swap(expected& rhs) noexcept(see below);
    

    -1- Constraints: […]

    -2- Effects: See Table 64 [tab:expected.object.swap].

    For the case where rhs.value() is false and this->has_value() is true, equivalent to:

    if constexpr (is_nothrow_move_constructible_v<E>) {
      E tmp(std::move(rhs.unex));
      destroy_at(addressof(rhs.unex));
      try {
        construct_at(addressof(rhs.val), std::move(val));
        destroy_at(addressof(val));
        construct_at(addressof(unex), std::move(tmp));
      } catch(...) {
        construct_at(addressof(rhs.unex), std::move(tmp));
        throw;
      }
    } else {
      remove_cv_t<T> tmp(std::move(val));
      destroy_at(addressof(val));
      try {
        construct_at(addressof(unex), std::move(rhs.unex));
        destroy_at(addressof(rhs.unex));
        construct_at(addressof(rhs.val), std::move(tmp));
      } catch (...) {
        construct_at(addressof(val), std::move(tmp));
        throw;
      }
    }
    has_val = false;
    rhs.has_val = true;
    
Date: 2024-10-15.00:00:00

[ 2024-10-02; Jonathan provides improved wording ]

Removed the use of `value()` in the [expected.object.swap] p2 Effects: and added `remove_cv_t` to the local `T` in the `else`-branch.

Date: 2023-03-15.00:00:00

[ 2023-03-22; Reflector poll ]

Set priority to 2 after reflector poll. "Not clear if all these wording changes are needed or desired." "Unconvinced that the mixed-value-error swap should use value(), source is destroyed immediately anyway. The else branch should use remove_cv_t too."

This wording is relative to N4928.

[Drafting note: When assignment and swap need to backup the old value by move construction, the source should be considered cv-unqualified, as the backup mechanism is only used internally.]

  1. Modify [expected.object.general] as indicated:

    […]
    bool has_val;      // exposition only
    union {
      remove_cv_t<T> val;       // exposition only
      E unex;          // exposition only
    };
    […]
    
  2. Modify [expected.object.assign] as indicated:

    constexpr expected& operator=(const expected& rhs);
    

    -2- Effects:

    1. (2.1) — If this->has_value() && rhs.has_value() is true, equivalent to value()val = *rhs.

    2. […]

    […]
    constexpr expected& operator=(expected&& rhs) noexcept(see below);
    

    […]

    -6- Effects:

    1. (6.1) — If this->has_value() && rhs.has_value() is true, equivalent to value()val = std::move(*rhs).

    2. […]

    […]
    template<class U = T>
      constexpr expected& operator=(U&& v);
    

    […]

    -10- Effects:

    1. (10.1) — If has_value() is true, equivalent to value()val = std::forward<U>(v).

    2. […]

  3. Modify Table 64: swap(expected&) effects [tab:expected.object.swap] as indicated:

    Table 64 — swap(expected&) effects [tab:expected.object.swap]
    this->has_value() !this->has_value()
    rhs.has_value() equivalent to: using std::swap;
    swap(value()val, rhs.value()val);
    calls rhs.swap(*this)
    […]
  4. Modify [expected.object.swap] as indicated:

    constexpr void swap(expected& rhs) noexcept(see below);
    

    -1- Constraints: […]

    -2- Effects: See Table 64 [tab:expected.object.swap].

    For the case where rhs.value() is false and this->has_value() is true, equivalent to:

    if constexpr (is_nothrow_move_constructible_v<E>) {
      E tmp(std::move(rhs.unex));
      destroy_at(addressof(rhs.unex));
      try {
        construct_at(addressof(rhs.val), std::move(value()val));
        destroy_at(addressof(val));
        construct_at(addressof(unex), std::move(tmp));
      } catch(...) {
        construct_at(addressof(rhs.unex), std::move(tmp));
        throw;
      }
    } else {
      T tmp(std::move(val));
      destroy_at(addressof(val));
      try {
        construct_at(addressof(unex), std::move(rhs.unex));
        destroy_at(addressof(rhs.unex));
        construct_at(addressof(rhs.val), std::move(tmp));
      } catch (...) {
        construct_at(addressof(val), std::move(tmp));
        throw;
      }
    }
    has_val = false;
    rhs.has_val = true;
    
Date: 2024-10-02.11:44:43

Currently the value_type of std::expected can be a cv-qualified type, which is possibly intended. However, LWG 3870 disallows std::construct_at to construct objects via cv T*, which breaks std::expected<cv T, E> because some operations are specified with std::construct_at ([expected.object.assign], [expected.object.swap]).

I think when T is cv-qualified, it would be better to store std::remove_cv_t<T> subobject while sometimes (other than construction/destruction) access it via a cv-qualified glvalue, which can also avoid UB associated with const/volatile objects.

History
Date User Action Args
2024-10-02 11:44:43adminsetmessages: + msg14397
2023-03-22 22:40:39adminsetmessages: + msg13478
2023-02-20 11:57:11adminsetmessages: + msg13428
2023-02-19 00:00:00admincreate