]> git.ipfire.org Git - thirdparty/gcc.git/commitdiff
libstdc++: Make std::expected trivially copy/move assignable (LWG 4026)
authorJonathan Wakely <jwakely@redhat.com>
Wed, 28 Jan 2026 12:33:46 +0000 (12:33 +0000)
committerJonathan Wakely <redi@gcc.gnu.org>
Thu, 29 Jan 2026 20:17:40 +0000 (20:17 +0000)
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.

libstdc++-v3/include/std/expected
libstdc++-v3/testsuite/20_util/expected/requirements.cc

index 948c2cbe6085e1d02757de51b09efde0d19328a2..7ab4e4595faca389cd72d0c32202c3a8d5d41d37 100644 (file)
@@ -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();
index c7ef5b603bf7e0430924c6ffed5e5145f2a2a7c0..3f6c84e82f9b89f99d3bf1461e25c4782313b400 100644 (file)
@@ -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>{}));