Created on 2023-02-19.00:00:00 last changed yesterday
Proposed resolution:
This wording is relative to N5032.
[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.]
Modify [expected.object.general] as indicated:
[…]
bool has_val; // exposition only
union {
remove_cv_t<T> val; // exposition only
E unex; // exposition only
};
[…]
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;
[ Croydon 2026-03-28; Status changed: Immediate → WP. ]
[ Croydon 2026-03-27; Status changed: New → Immediate. ]
[ Croydon 2026-03-27; Jonathan provides new wording ]
LWG review pointed out that using `value()` relies on a freestanding-deleted
function. The equivalent functions in `optional` just use `val = rhs.val` and
we can do the same here. We don't need to disallow assignment to `const T`
here in the general case, because the assignments and swaps are already
constrained to only work if the type is assignable/swappable.
If your type satisfies both
assignable_from<const T&, const T&>
and assignable_from<T&, const T&>
but with different semantics, this function might have surprising effects.
That seems fine.
[ 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.
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.]
Modify [expected.object.general] as indicated:
[…]
bool has_val; // exposition only
union {
remove_cv_t<T> val; // exposition only
E unex; // exposition only
};
[…]
Modify [expected.object.assign] as indicated:
constexpr expected& operator=(const expected& rhs);[…]-2- Effects:
(2.1) — If this->has_value() && rhs.has_value() is true, equivalent to value()
val= *rhs.[…]
constexpr expected& operator=(expected&& rhs) noexcept(see below);[…][…]
-6- Effects:
(6.1) — If this->has_value() && rhs.has_value() is true, equivalent to value()
val= std::move(*rhs).[…]
template<class U = T> constexpr expected& operator=(U&& v);[…]
-10- Effects:
(10.1) — If has_value() is true, equivalent to value()
val= std::forward<U>(v).[…]
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) […]
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;
[ 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.]
Modify [expected.object.general] as indicated:
[…]
bool has_val; // exposition only
union {
remove_cv_t<T> val; // exposition only
E unex; // exposition only
};
[…]
Modify [expected.object.assign] as indicated:
constexpr expected& operator=(const expected& rhs);[…]-2- Effects:
(2.1) — If this->has_value() && rhs.has_value() is true, equivalent to value()
val= *rhs.[…]
constexpr expected& operator=(expected&& rhs) noexcept(see below);[…][…]
-6- Effects:
(6.1) — If this->has_value() && rhs.has_value() is true, equivalent to value()
val= std::move(*rhs).[…]
template<class U = T> constexpr expected& operator=(U&& v);[…]
-10- Effects:
(10.1) — If has_value() is true, equivalent to value()
val= std::forward<U>(v).[…]
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) […]
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;
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 |
| 2026-03-31 16:35:58 | admin | set | messages: + msg16189 |
| 2026-03-31 16:35:58 | admin | set | status: immediate -> wp |
| 2026-03-27 14:32:03 | admin | set | messages: + msg16126 |
| 2026-03-27 14:32:03 | admin | set | status: new -> immediate |
| 2026-03-27 13:57:36 | admin | set | messages: + msg16118 |
| 2024-10-02 11:44:43 | admin | set | messages: + msg14397 |
| 2023-03-22 22:40:39 | admin | set | messages: + msg13478 |
| 2023-02-20 11:57:11 | admin | set | messages: + msg13428 |
| 2023-02-19 00:00:00 | admin | create | |