On Thu, Jan 29, 2026 at 2:57 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.
>
> As shown in the updates to the test, defaulted special members can have
> noexcept(false) even if they would be noexcept(true) by default. The new
> defaulted operator= overloads added by this commit have conditional
> noexcept-specifiers that propagate any noexcept(false) on trivial
> special members of the T and E types. This is probably what users
> expect, if they've bothered to put noexcept(false) on their defaulted
> special members.
>
> libstdc++-v3/ChangeLog:
>
>         * include/std/expected (__expected::__trivially_replaceable)
>         (__expected::__usable_for_assign)
>         (__expected::__usable_for_trivial_assign)
>         (__expected::__can_reassign_type): 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 properties of assignments.
> ---
>
> v2: Renamed __can_flip_state concept to __can_reassign_type. Added tests
> for propagating noexcept(false) on trivial assignments and updated
> commit message to explain that.
>
> Tested x86_64-linux.
>
LGTM.

>
>  libstdc++-v3/include/std/expected             |  89 +++++++++--
>  .../20_util/expected/requirements.cc          | 138 ++++++++++++++----
>  2 files changed, 190 insertions(+), 37 deletions(-)
>
> diff --git a/libstdc++-v3/include/std/expected
> b/libstdc++-v3/include/std/expected
> index 948c2cbe6085..7ab4e4595fac 100644
> --- a/libstdc++-v3/include/std/expected
> +++ b/libstdc++-v3/include/std/expected
> @@ -340,6 +340,29 @@ 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>;
> +
> +  // _GLIBCXX_RESOLVE_LIB_DEFECTS
> +  // 4026. Assignment operators of std::expected should propagate
> triviality
> +  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_reassign_type
> +      = is_nothrow_move_constructible_v<_Tp>
> +         || is_nothrow_move_constructible_v<_Er>;
>  }
>  /// @endcond
>
> @@ -560,18 +583,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_reassign_type<_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_reassign_type<_Tp, _Er>
>        {
>         if (__x._M_has_value)
>           this->_M_assign_val(__x._M_val);
> @@ -580,16 +616,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_reassign_type<_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_reassign_type<_Tp, _Er>
>        {
>         if (__x._M_has_value)
>           _M_assign_val(std::move(__x._M_val));
> @@ -1447,14 +1495,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 +1520,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..3f6c84e82f9b 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 );
> @@ -126,3 +153,64 @@ static_assert( sizeof(std::expected<void, char>) == 2
> );
>  static_assert( sizeof(std::expected<void*, char>) == sizeof(void*) +
> __alignof(void*) );
>  static_assert( alignof(std::expected<void, char>) == 1 );
>  static_assert( alignof(std::expected<void*, char>) == alignof(void*) );
> +
> +// For QoI we propagate noexcept(false) from trivial special members.
>
This is QoI, but we are matching what standard specifies (it specifies
noexcept
and says that constructor with given noexcept is trivial), we do use our
freedom
to strengthen it.

> +template<bool CopyCtor, bool MoveCtor, bool CopyAssign, bool MoveAssign>
> +struct Y {
> +    Y(const Y&) noexcept(CopyCtor) = default;
> +    Y(Y&&) noexcept(MoveCtor) = default;
> +    Y& operator=(const Y&) noexcept(CopyAssign) = default;
> +    Y& operator=(Y&&) noexcept(MoveAssign) = default;
> +};
> +
> +template<int I> using Yi = Y<bool(I&8), bool(I&4), bool(I&2), bool(I&1)>;
> +
> +template<typename> constexpr bool nothrow_copy = false;
> +template<typename> constexpr bool nothrow_move = false;
> +
> +template<bool CC, bool MC, bool CA, bool MA>
> +constexpr bool nothrow_copy<Y<CC, MC, CA, MA>> = CC && CA;
> +
> +template<bool CC, bool MC, bool CA, bool MA>
> +constexpr bool nothrow_move<Y<CC, MC, CA, MA>> = MC && MA;
> +
> +template<> constexpr bool nothrow_copy<void> = true;
> +template<> constexpr bool nothrow_move<void> = true;
> +
> +template<typename A, typename B>
> +consteval bool do_checks()
> +{
> +  if constexpr (std::is_void_v<A> ||
> std::is_nothrow_move_constructible_v<A>
> +               || std::is_nothrow_move_constructible_v<B>)
> +    {
> +      // All assignments should be trivial
> +      static_assert( copy_assignable<A, B> == Trivial );
> +      static_assert( move_assignable<A, B> == Trivial );
> +      // But whether they are nothrow depends on the noexcept-specifiers
> +      static_assert( std::is_nothrow_copy_assignable_v<std::expected<A,
> B>>
> +                   == (nothrow_copy<A> && nothrow_copy<B>) );
> +      static_assert( std::is_nothrow_move_assignable_v<std::expected<A,
> B>>
> +                   == (nothrow_move<A> && nothrow_move<B>) );
> +    }
> +  else
> +    {
> +      static_assert( copy_assignable<A, B> == No );
> +      static_assert( move_assignable<A, B> == No );
> +    }
> +  return true;
> +}
> +
> +template<typename A, int... I>
> +consteval bool check(std::integer_sequence<int, I...>)
> +{
> +  return (do_checks<A, Yi<I>>() && ...);
> +}
> +
> +template<int... I>
> +consteval bool
> +check_all(std::integer_sequence<int, I...> i)
> +{
> +  return (check<Yi<I>>(i) && ...) && check<void>(i);
> +}
> +
> +static_assert(check_all(std::make_integer_sequence<int, 16>{}));
> --
> 2.52.0
>
>

Reply via email to