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.

3891. LWG 3870 breaks std::expected<cv T, E>

Section: 22.8.6.1 [expected.object.general] Status: New Submitter: Jiang An Opened: 2023-02-19 Last modified: 2024-10-02

Priority: 2

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

View all issues with New status.

Discussion:

Currently the value_type of std::expected can be a cv-qualified type, which is possibly intended. However, LWG 3870(i) 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 (22.8.6.4 [expected.object.assign], 22.8.6.5 [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.

[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."

Previous resolution [SUPERSEDED]:

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 22.8.6.1 [expected.object.general] as indicated:

    […]
    bool has_val;      // exposition only
    union {
      remove_cv_t<T> val;       // exposition only
      E unex;          // exposition only
    };
    […]
    
  2. Modify 22.8.6.4 [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 22.8.6.5 [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;
    

[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.

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 22.8.6.1 [expected.object.general] as indicated:

    […]
    bool has_val;      // exposition only
    union {
      remove_cv_t<T> val;       // exposition only
      E unex;          // exposition only
    };
    […]
    
  2. Modify 22.8.6.4 [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 22.8.6.5 [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;