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>{}));
