This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of WP status.
std::expected<cv T, E>Section: 22.8.6.1 [expected.object.general] Status: WP Submitter: Jiang An Opened: 2023-02-19 Last modified: 2026-03-31
Priority: 2
View all other issues in [expected.object.general].
View all issues with WP 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]).
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
swapneed to backup the old value by move construction, the source should be considered cv-unqualified, as the backup mechanism is only used internally.]
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 }; […]Modify 22.8.6.4 [expected.object.assign] as indicated:
constexpr expected& operator=(const expected& rhs);[…]-2- Effects:
(2.1) — If
this->has_value() && rhs.has_value()istrue, equivalent tovalue().val= *rhs[…]
constexpr expected& operator=(expected&& rhs) noexcept(see below);[…][…]
-6- Effects:
(6.1) — If
this->has_value() && rhs.has_value()istrue, equivalent tovalue().val= std::move(*rhs)[…]
template<class U = T> constexpr expected& operator=(U&& v);[…]
-10- Effects:
(10.1) — If
has_value()istrue, equivalent tovalue().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 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 whererhs.value()isfalseandthis->has_value()istrue, 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.
Previous resolution [SUPERSEDED]:
This wording is relative to N4988.
[Drafting note: When assignment and
swapneed to backup the old value by move construction, the source should be considered cv-unqualified, as the backup mechanism is only used internally.]
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 }; […]Modify 22.8.6.4 [expected.object.assign] as indicated:
constexpr expected& operator=(const expected& rhs);[…]-2- Effects:
(2.1) — If
this->has_value() && rhs.has_value()istrue, equivalent tovalue().val= *rhs[…]
constexpr expected& operator=(expected&& rhs) noexcept(see below);[…][…]
-6- Effects:
(6.1) — If
this->has_value() && rhs.has_value()istrue, equivalent tovalue().val= std::move(*rhs)[…]
template<class U = T> constexpr expected& operator=(U&& v);[…]
-10- Effects:
(10.1) — If
has_value()istrue, equivalent tovalue().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 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 whererhs.value()isfalseandthis->has_value()istrue, 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-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.
[Croydon 2026-03-27; Status changed: New → Immediate.]
[Croydon 2026-03-28; Status changed: Immediate → WP.]
Proposed resolution:
This wording is relative to N5032.
[Drafting note: When assignment and
swapneed to backup the old value by move construction, the source should be considered cv-unqualified, as the backup mechanism is only used internally.]
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
};
[…]
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 whererhs.value()isfalseandthis->has_value()istrue, 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;