https://gcc.gnu.org/g:57f571f7283c72e958f59090f3699bf0111b6bfd

commit r16-7159-g57f571f7283c72e958f59090f3699bf0111b6bfd
Author: Jonathan Wakely <[email protected]>
Date:   Wed Jan 28 12:33:46 2026 +0000

    libstdc++: Make std::expected trivially copy/move assignable (LWG 4026)
    
    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 match the conditions of the non-trivial
    assignments, propagating any noexcept(false) on trivial special members
    of the T and E types. We could strengthen the noexcept for the trivial
    operators, but propagating the conditions from the underlying types 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.

Diff:
---
 libstdc++-v3/include/std/expected                  |  89 +++++++++++--
 .../testsuite/20_util/expected/requirements.cc     | 142 +++++++++++++++++----
 2 files changed, 192 insertions(+), 39 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>>;
-
-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 > );
+  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 > == 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.
+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>{}));

Reply via email to