Per discussion on LWG4513 (PR), the issue was duplicate of LWG4026.
On Wed, Jan 28, 2026 at 9:38 PM Jonathan Wakely <[email protected]> wrote:
> This is the subject of two NB comments on C++26 which seem likely to be
> approved. We're allowed to make this change as QoI anyway, even if it
> isn't approved for the standard, and it should apply to C++23 as well to
> avoid ABI changes between C++23 and C++26.
>
Also mention here why we need noexcept declaration, as the default one will
take only `noexcept` in assignment into consideration.
>
> libstdc++-v3/ChangeLog:
>
> * include/std/expected (__expected::__trivially_replaceable)
> (__expected::__usable_for_assign)
> (__expected::__usable_for_trivial_assign)
> (__expected::__can_flip_state): New concepts.
> (expected::operator=): Adjust constraints
> on existing overloads and add defaulted overload.
> (expected<cv void, E>::operator=): Likewise.
> * testsuite/20_util/expected/requirements.cc: Check for trivial
> and nothrow assignments.
> ---
>
> Tested x86_64-linux.
>
> libstdc++-v3/include/std/expected | 87 ++++++++++++++++---
> .../20_util/expected/requirements.cc | 77 ++++++++++------
> 2 files changed, 127 insertions(+), 37 deletions(-)
>
> diff --git a/libstdc++-v3/include/std/expected
> b/libstdc++-v3/include/std/expected
> index 948c2cbe6085..a41a8f2572dd 100644
> --- a/libstdc++-v3/include/std/expected
> +++ b/libstdc++-v3/include/std/expected
> @@ -340,6 +340,27 @@ namespace __expected
> concept __not_constructing_bool_from_expected
> = ! is_same_v<remove_cv_t<_Tp>, bool>
> || ! __is_expected<remove_cvref_t<_Up>>;
> +
> + template<typename _Tp, typename _Up = remove_cvref_t<_Tp>>
> + concept __trivially_replaceable
> + = is_trivially_constructible_v<_Up, _Tp>
> + && is_trivially_assignable_v<_Up&, _Tp>
> + && is_trivially_destructible_v<_Up>;
> +
> + template<typename _Tp, typename _Up = remove_cvref_t<_Tp>>
> + concept __usable_for_assign
> + = is_constructible_v<_Up, _Tp> && is_assignable_v<_Up&, _Tp>;
> +
> + template<typename _Tp>
> + concept __usable_for_trivial_assign
> + = __trivially_replaceable<_Tp> && __usable_for_assign<_Tp>;
> +
> + // For copy/move assignment to replace T with E, or vice versa,
> + // we require at least one of them to be nothrow move constructible.
> + template<typename _Tp, typename _Er>
> + concept __can_flip_state
> + = is_nothrow_move_constructible_v<_Tp>
> + || is_nothrow_move_constructible_v<_Er>;
> }
> /// @endcond
>
> @@ -560,18 +581,31 @@ namespace __expected
>
> // assignment
>
> + // Deleted copy assignment, when constraints not met for other
> overloads
> expected& operator=(const expected&) = delete;
>
> + // Trivial copy assignment
> + expected&
> + operator=(const expected&)
> + noexcept(__and_v<is_nothrow_copy_constructible<_Tp>,
> + is_nothrow_copy_constructible<_Er>,
> + is_nothrow_copy_assignable<_Tp>,
> + is_nothrow_copy_assignable<_Er>>)
> + requires __expected::__usable_for_trivial_assign<const _Tp&>
> + && __expected::__usable_for_trivial_assign<const _Er&>
> + && __expected::__can_flip_state<_Tp, _Er>
> + = default;
> +
> + // Non-trivial copy assignment
> constexpr expected&
> operator=(const expected& __x)
> noexcept(__and_v<is_nothrow_copy_constructible<_Tp>,
> is_nothrow_copy_constructible<_Er>,
> is_nothrow_copy_assignable<_Tp>,
> is_nothrow_copy_assignable<_Er>>)
> - requires is_copy_assignable_v<_Tp> && is_copy_constructible_v<_Tp>
> - && is_copy_assignable_v<_Er> && is_copy_constructible_v<_Er>
> - && (is_nothrow_move_constructible_v<_Tp>
> - || is_nothrow_move_constructible_v<_Er>)
> + requires __expected::__usable_for_assign<const _Tp&>
> + && __expected::__usable_for_assign<const _Er&>
> + && __expected::__can_flip_state<_Tp, _Er>
> {
> if (__x._M_has_value)
> this->_M_assign_val(__x._M_val);
> @@ -580,16 +614,28 @@ namespace __expected
> return *this;
> }
>
> + // Trivial move assignment
> + expected&
> + operator=(expected&&)
> + noexcept(__and_v<is_nothrow_move_constructible<_Tp>,
> + is_nothrow_move_constructible<_Er>,
> + is_nothrow_move_assignable<_Tp>,
> + is_nothrow_move_assignable<_Er>>)
> + requires __expected::__usable_for_trivial_assign<_Tp&&>
> + && __expected::__usable_for_trivial_assign<_Er&&>
> + && __expected::__can_flip_state<_Tp, _Er>
> + = default;
> +
> + // Non-trivial move assignment
> constexpr expected&
> operator=(expected&& __x)
> noexcept(__and_v<is_nothrow_move_constructible<_Tp>,
> is_nothrow_move_constructible<_Er>,
> is_nothrow_move_assignable<_Tp>,
> is_nothrow_move_assignable<_Er>>)
> - requires is_move_assignable_v<_Tp> && is_move_constructible_v<_Tp>
> - && is_move_assignable_v<_Er> && is_move_constructible_v<_Er>
> - && (is_nothrow_move_constructible_v<_Tp>
> - || is_nothrow_move_constructible_v<_Er>)
> + requires __expected::__usable_for_assign<_Tp&&>
> + && __expected::__usable_for_assign<_Er&&>
> + && __expected::__can_flip_state<_Tp, _Er>
> {
> if (__x._M_has_value)
> _M_assign_val(std::move(__x._M_val));
> @@ -1447,14 +1493,23 @@ namespace __expected
>
> // assignment
>
> + // Deleted copy assignment, when constraints not met for other
> overloads
> expected& operator=(const expected&) = delete;
>
> + // Trivial copy assignment
> + expected&
> + operator=(const expected&)
> + noexcept(__and_v<is_nothrow_copy_constructible<_Er>,
> + is_nothrow_copy_assignable<_Er>>)
> + requires __expected::__usable_for_trivial_assign<const _Er&>
> + = default;
> +
> + // Non-trivial copy assignment
> constexpr expected&
> operator=(const expected& __x)
> noexcept(__and_v<is_nothrow_copy_constructible<_Er>,
> is_nothrow_copy_assignable<_Er>>)
> - requires is_copy_constructible_v<_Er>
> - && is_copy_assignable_v<_Er>
> + requires __expected::__usable_for_assign<const _Er&>
> {
> if (__x._M_has_value)
> emplace();
> @@ -1463,12 +1518,20 @@ namespace __expected
> return *this;
> }
>
> + // Trivial move assignment
> + expected&
> + operator=(expected&&)
> + noexcept(__and_v<is_nothrow_move_constructible<_Er>,
> + is_nothrow_move_assignable<_Er>>)
> + requires __expected::__usable_for_trivial_assign<_Er&&>
> + = default;
> +
> + // Non-trivial move assignment
> constexpr expected&
> operator=(expected&& __x)
> noexcept(__and_v<is_nothrow_move_constructible<_Er>,
> is_nothrow_move_assignable<_Er>>)
> - requires is_move_constructible_v<_Er>
> - && is_move_assignable_v<_Er>
> + requires __expected::__usable_for_assign<_Er&&>
> {
> if (__x._M_has_value)
> emplace();
> diff --git a/libstdc++-v3/testsuite/20_util/expected/requirements.cc
> b/libstdc++-v3/testsuite/20_util/expected/requirements.cc
> index c7ef5b603bf7..d807724b5470 100644
> --- a/libstdc++-v3/testsuite/20_util/expected/requirements.cc
> +++ b/libstdc++-v3/testsuite/20_util/expected/requirements.cc
> @@ -86,39 +86,66 @@ static_assert( move_constructible< void, E > ==
> NoThrow );
> // Copy assignment
>
> template<typename T, typename E>
> - constexpr bool copy_assignable
> - = std::is_copy_assignable_v<std::expected<T, E>>;
> + constexpr Result copy_assignable
> + = std::is_trivially_copy_assignable_v<std::expected<T, E>> ? Trivial
> + : std::is_nothrow_copy_assignable_v<std::expected<T, E>> ? NoThrow
> + : std::is_copy_assignable_v<std::expected<T, E>> ? Yes
> + : No;
>
> struct F { F(F&&); F& operator=(const F&); }; // not copy-constructible
> -struct G { G(const G&); G(G&&); G& operator=(const G&); }; // throwing
> move
>
> -static_assert( copy_assignable< int, int > );
> -static_assert( copy_assignable< F, int > == false );
> -static_assert( copy_assignable< int, F > == false );
> -static_assert( copy_assignable< F, F > == false );
> -static_assert( copy_assignable< G, int > );
> -static_assert( copy_assignable< int, G > );
> -static_assert( copy_assignable< G, G > == false );
> -static_assert( copy_assignable< void, int > );
> -static_assert( copy_assignable< void, F > == false );
> -static_assert( copy_assignable< void, G > );
> +template<bool CopyCtor, bool MoveCtor, bool CopyAssign, bool MoveAssign>
> +struct X {
> + X(const X&) noexcept(CopyCtor);
> + X(X&&) noexcept(MoveCtor);
> + X& operator=(const X&) noexcept(CopyAssign);
> + X& operator=(X&&) noexcept(MoveAssign);
> +};
> +using G = X<false, false, false, false>;
> +using H = X<false, true, true, true>;
> +using I = X<true, true, true, false>;
> +
> +static_assert( copy_assignable< int, int > == Trivial );
> +static_assert( copy_assignable< F, int > == No );
> +static_assert( copy_assignable< int, F > == No );
> +static_assert( copy_assignable< F, F > == No );
> +static_assert( copy_assignable< G, int > == Yes );
> +static_assert( copy_assignable< int, G > == Yes );
> +static_assert( copy_assignable< G, G > == No );
> +static_assert( copy_assignable< int, H > == Yes );
> +static_assert( copy_assignable< H, H > == Yes );
> +static_assert( copy_assignable< int, I > == NoThrow );
> +static_assert( copy_assignable< I, I > == NoThrow );
> +static_assert( copy_assignable< void, int > == Trivial );
> +static_assert( copy_assignable< void, F > == No );
> +static_assert( copy_assignable< void, G > == Yes );
> +static_assert( copy_assignable< void, H > == Yes );
> +static_assert( copy_assignable< void, I > == NoThrow );
>
> // Move assignment
>
> template<typename T, typename E>
> - constexpr bool move_assignable
> - = std::is_move_assignable_v<std::expected<T, E>>;
> + constexpr Result move_assignable
> + = std::is_trivially_move_assignable_v<std::expected<T, E>> ? Trivial
> + : std::is_nothrow_move_assignable_v<std::expected<T, E>> ? NoThrow
> + : std::is_move_assignable_v<std::expected<T, E>> ? Yes
> + : No;
>
> -static_assert( move_assignable< int, int > );
> -static_assert( move_assignable< F, int > );
> -static_assert( move_assignable< int, F > );
> -static_assert( move_assignable< F, F > == false );
> -static_assert( move_assignable< G, int > );
> -static_assert( move_assignable< int, G > );
> -static_assert( move_assignable< G, G > == false );
> -static_assert( move_assignable< void, int > );
> -static_assert( move_assignable< void, F > );
> -static_assert( move_assignable< void, G > );
> +static_assert( move_assignable< int, int > == Trivial );
> +static_assert( move_assignable< F, int > == Yes );
> +static_assert( move_assignable< int, F > == Yes );
> +static_assert( move_assignable< F, F > == No );
> +static_assert( move_assignable< G, int > == Yes );
> +static_assert( move_assignable< int, G > == Yes );
> +static_assert( move_assignable< G, G > == No );
> +static_assert( move_assignable< int, H > == NoThrow );
> +static_assert( move_assignable< H, H > == NoThrow );
> +static_assert( move_assignable< I, I > == Yes );
> +static_assert( move_assignable< void, int > == Trivial );
> +static_assert( move_assignable< void, F > == Yes );
> +static_assert( move_assignable< void, G > == Yes );
> +static_assert( move_assignable< void, H > == NoThrow );
> +static_assert( move_assignable< void, I > == Yes );
>
> // QoI properties
> static_assert( sizeof(std::expected<char, unsigned char>) == 2 );
> --
> 2.52.0
>
>