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

Section: [expected.object.general] Status: New Submitter: Jiang An Opened: 2023-02-19 Last modified: 2023-03-22 22:36:03 UTC

Priority: 2

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

View all issues with New status.


Currently the value_type of std::expected can be a cv-qualified type, which is possibly intented. 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.

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

Proposed resolution:

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));
      try {
        construct_at(addressof(rhs.val), std::move(value()val));
        construct_at(addressof(unex), std::move(tmp));
      } catch(...) {
        construct_at(addressof(rhs.unex), std::move(tmp));
    } else {
      T tmp(std::move(val));
      try {
        construct_at(addressof(unex), std::move(rhs.unex));
        construct_at(addressof(rhs.val), std::move(tmp));
      } catch (...) {
        construct_at(addressof(val), std::move(tmp));
    has_val = false;
    rhs.has_val = true;