From: Tomasz Kamiński Date: Fri, 16 Jan 2026 13:01:53 +0000 (+0100) Subject: libstdc++: Use overload operator<=> when provided in relational functors [PR114153] X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8fad43b7850a99b32c48570fc2a3d8ae5a76542a;p=thirdparty%2Fgcc.git libstdc++: Use overload operator<=> when provided in relational functors [PR114153] The implementation of less<> did not consider the possibility of t < u being rewritten from overloaded operator<=>. This lead to situation when for t,u that: * provide overload operator<=>, such that (t < u) is rewritten to (t <=> u) < 0, * are convertible to pointers, the expression std::less<>(t, u) would incorrectly result in call of std::less on values converted to the pointers, instead of t < u. The similar issues also occurred for greater<>, less_equal<>, greater_equal<>, their range equivalents, and in three_way_compare for heterogeneous calls. This patch addresses above, by also checking for free-functions and member overloads of operator<=>, before falling back to pointer comparison. We do not put any constraints on the return type of selected operator, in particular in being one of the standard defined comparison categories, as the language does not put any restriction of returned type, and if (t <=> u) is well formed, (t op u) is interpreted as (t <=> u) op 0. If that later expression is ill-formed, the expression using op also is (see included tests). The relational operator rewrites try both order of arguments, t < u, can be rewritten into operator<=>(t, u) < 0 or 0 < operator<=>(u, t), it means that we need to test both operator<=>(T, U) and operator<=>(U, T) if T and U are not the same types. This is now extracted into __not_overloaded_spaceship helper concept, placed in , to avoid extending set of includes. The compare_three_way functor defined in compare, already considers overloaded operator<=>, however it does not consider reversed candidates, leading to situation in which t <=> u results in 0 <=> operator<=>(u, t), while compare_three_way{}(t, u) uses pointer comparison. This is also addressed by using __not_overloaded_spaceship, that check both order of arguments. Finally, as operator<=> is introduced in C++20, for std::less(_equal)?<>, std::greater(_equal)?<>, we use provide separate __ptr_cmp implementation in that mode, that relies on use of requires expression. We use a nested requires clause to guarantee short-circuiting of their evaluation. The operator() of aforementioned functors is reworked to use if constexpr, in all standard modes (as we allow is as extension), eliminating the need for _S_cmp function. PR libstdc++/114153 libstdc++-v3/ChangeLog: * include/bits/ranges_cmp.h (__detail::__less_builtin_ptr_cmp): Add __not_overloaded_spaceship spaceship check. * include/bits/stl_function.h (greater::operator()) (less::operator(), greater_equal::operator()) (less_equal::operator()): Implement using if constexpr. (greater::__S_cmp, less::__S_cmp) (greater_equal::__ptr_comp, less_equal::S_cmp): Remove. (greater::__ptr_cmp, less::__ptr_cmp) (greater_equal::__ptr_comp, less_equal::ptr_cmp): Change tostatic constexpr variable. Define in terms of requires expressions and __not_overloaded_spaceship check. * include/std/concepts: (__detail::__not_overloaded_spaceship): Define. * libsupc++/compare: (__detail::__3way_builtin_ptr_cmp): Use __not_overloaded_spaceship concept. * testsuite/20_util/function_objects/comparisons_pointer_spaceship.cc: New test. Reviewed-by: Jonathan Wakely Signed-off-by: Tomasz Kamiński --- diff --git a/libstdc++-v3/include/bits/ranges_cmp.h b/libstdc++-v3/include/bits/ranges_cmp.h index c6451427beb..e2bf97ed4cd 100644 --- a/libstdc++-v3/include/bits/ranges_cmp.h +++ b/libstdc++-v3/include/bits/ranges_cmp.h @@ -71,10 +71,11 @@ namespace ranges = requires (_Tp&& __t, _Up&& __u) { { __t < __u } -> same_as; } && convertible_to<_Tp, const volatile void*> && convertible_to<_Up, const volatile void*> - && (! requires(_Tp&& __t, _Up&& __u) + && ! requires(_Tp&& __t, _Up&& __u) { operator<(std::forward<_Tp>(__t), std::forward<_Up>(__u)); } - && ! requires(_Tp&& __t, _Up&& __u) - { std::forward<_Tp>(__t).operator<(std::forward<_Up>(__u)); }); + && ! requires(_Tp&& __t, _Up&& __u) + { std::forward<_Tp>(__t).operator<(std::forward<_Up>(__u)); } + && std::__detail::__not_overloaded_spaceship<_Tp, _Up>; } // namespace __detail // [range.cmp] Concept-constrained comparisons diff --git a/libstdc++-v3/include/bits/stl_function.h b/libstdc++-v3/include/bits/stl_function.h index 659025c4bf9..0600de72b10 100644 --- a/libstdc++-v3/include/bits/stl_function.h +++ b/libstdc++-v3/include/bits/stl_function.h @@ -59,6 +59,9 @@ #if __cplusplus > 201103L #include #endif +#if __cplusplus >= 202002L +#include +#endif namespace std _GLIBCXX_VISIBILITY(default) { @@ -525,8 +528,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION noexcept(noexcept(std::forward<_Tp>(__t) > std::forward<_Up>(__u))) -> decltype(std::forward<_Tp>(__t) > std::forward<_Up>(__u)) { - return _S_cmp(std::forward<_Tp>(__t), std::forward<_Up>(__u), - __ptr_cmp<_Tp, _Up>{}); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wc++17-extensions" // if constexpr + if constexpr (__ptr_cmp<_Tp, _Up>) + return greater{}( + static_cast(std::forward<_Tp>(__t)), + static_cast(std::forward<_Up>(__u))); + else + return std::forward<_Tp>(__t) > std::forward<_Up>(__u); +#pragma GCC diagnostic pop } template @@ -537,20 +547,20 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION typedef __is_transparent is_transparent; private: - template - static constexpr decltype(auto) - _S_cmp(_Tp&& __t, _Up&& __u, false_type) - { return std::forward<_Tp>(__t) > std::forward<_Up>(__u); } - - template - static constexpr bool - _S_cmp(_Tp&& __t, _Up&& __u, true_type) noexcept +#if __cplusplus >= 202002L + template + static constexpr bool __ptr_cmp = requires { - return greater{}( - static_cast(std::forward<_Tp>(__t)), - static_cast(std::forward<_Up>(__u))); - } - + requires + ! requires + { operator>(std::declval<_Tp>(), std::declval<_Up>()); } + && ! requires + { std::declval<_Tp>().operator>(std::declval<_Up>()); } + && __detail::__not_overloaded_spaceship<_Tp, _Up> + && is_convertible_v<_Tp, const volatile void*> + && is_convertible_v<_Up, const volatile void*>; + }; +#else // True if there is no viable operator> member function. template struct __not_overloaded2 : true_type { }; @@ -572,9 +582,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION : false_type { }; template - using __ptr_cmp = __and_<__not_overloaded<_Tp, _Up>, - is_convertible<_Tp, const volatile void*>, - is_convertible<_Up, const volatile void*>>; + static constexpr bool __ptr_cmp = __and_< + __not_overloaded<_Tp, _Up>, + is_convertible<_Tp, const volatile void*>, + is_convertible<_Up, const volatile void*>>::value; +#endif }; /// One of the @link comparison_functors comparison functors@endlink. @@ -587,8 +599,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION noexcept(noexcept(std::forward<_Tp>(__t) < std::forward<_Up>(__u))) -> decltype(std::forward<_Tp>(__t) < std::forward<_Up>(__u)) { - return _S_cmp(std::forward<_Tp>(__t), std::forward<_Up>(__u), - __ptr_cmp<_Tp, _Up>{}); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wc++17-extensions" // if constexpr + if constexpr (__ptr_cmp<_Tp, _Up>) + return less{}( + static_cast(std::forward<_Tp>(__t)), + static_cast(std::forward<_Up>(__u))); + else + return std::forward<_Tp>(__t) < std::forward<_Up>(__u); +#pragma GCC diagnostic pop } template @@ -599,20 +618,20 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION typedef __is_transparent is_transparent; private: - template - static constexpr decltype(auto) - _S_cmp(_Tp&& __t, _Up&& __u, false_type) - { return std::forward<_Tp>(__t) < std::forward<_Up>(__u); } - - template - static constexpr bool - _S_cmp(_Tp&& __t, _Up&& __u, true_type) noexcept +#if __cplusplus >= 202002L + template + static constexpr bool __ptr_cmp = requires { - return less{}( - static_cast(std::forward<_Tp>(__t)), - static_cast(std::forward<_Up>(__u))); - } - + requires + ! requires + { operator<(std::declval<_Tp>(), std::declval<_Up>()); } + && ! requires + { std::declval<_Tp>().operator<(std::declval<_Up>()); } + && __detail::__not_overloaded_spaceship<_Tp, _Up> + && is_convertible_v<_Tp, const volatile void*> + && is_convertible_v<_Up, const volatile void*>; + }; +#else // True if there is no viable operator< member function. template struct __not_overloaded2 : true_type { }; @@ -634,9 +653,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION : false_type { }; template - using __ptr_cmp = __and_<__not_overloaded<_Tp, _Up>, - is_convertible<_Tp, const volatile void*>, - is_convertible<_Up, const volatile void*>>; + static constexpr bool __ptr_cmp = __and_< + __not_overloaded<_Tp, _Up>, + is_convertible<_Tp, const volatile void*>, + is_convertible<_Up, const volatile void*>>::value; +#endif }; /// One of the @link comparison_functors comparison functors@endlink. @@ -649,8 +670,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION noexcept(noexcept(std::forward<_Tp>(__t) >= std::forward<_Up>(__u))) -> decltype(std::forward<_Tp>(__t) >= std::forward<_Up>(__u)) { - return _S_cmp(std::forward<_Tp>(__t), std::forward<_Up>(__u), - __ptr_cmp<_Tp, _Up>{}); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wc++17-extensions" // if constexpr + if constexpr (__ptr_cmp<_Tp, _Up>) + return greater_equal{}( + static_cast(std::forward<_Tp>(__t)), + static_cast(std::forward<_Up>(__u))); + else + return std::forward<_Tp>(__t) >= std::forward<_Up>(__u); +#pragma GCC diagnostic pop } template @@ -661,20 +689,20 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION typedef __is_transparent is_transparent; private: - template - static constexpr decltype(auto) - _S_cmp(_Tp&& __t, _Up&& __u, false_type) - { return std::forward<_Tp>(__t) >= std::forward<_Up>(__u); } - - template - static constexpr bool - _S_cmp(_Tp&& __t, _Up&& __u, true_type) noexcept +#if __cplusplus >= 202002L + template + static constexpr bool __ptr_cmp = requires { - return greater_equal{}( - static_cast(std::forward<_Tp>(__t)), - static_cast(std::forward<_Up>(__u))); - } - + requires + ! requires + { operator>=(std::declval<_Tp>(), std::declval<_Up>()); } + && ! requires + { std::declval<_Tp>().operator>=(std::declval<_Up>()); } + && __detail::__not_overloaded_spaceship<_Tp, _Up> + && is_convertible_v<_Tp, const volatile void*> + && is_convertible_v<_Up, const volatile void*>; + }; +#else // True if there is no viable operator>= member function. template struct __not_overloaded2 : true_type { }; @@ -696,9 +724,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION : false_type { }; template - using __ptr_cmp = __and_<__not_overloaded<_Tp, _Up>, - is_convertible<_Tp, const volatile void*>, - is_convertible<_Up, const volatile void*>>; + static constexpr bool __ptr_cmp = __and_< + __not_overloaded<_Tp, _Up>, + is_convertible<_Tp, const volatile void*>, + is_convertible<_Up, const volatile void*>>::value; +#endif }; /// One of the @link comparison_functors comparison functors@endlink. @@ -711,8 +741,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION noexcept(noexcept(std::forward<_Tp>(__t) <= std::forward<_Up>(__u))) -> decltype(std::forward<_Tp>(__t) <= std::forward<_Up>(__u)) { - return _S_cmp(std::forward<_Tp>(__t), std::forward<_Up>(__u), - __ptr_cmp<_Tp, _Up>{}); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wc++17-extensions" // if constexpr + if constexpr (__ptr_cmp<_Tp, _Up>) + return less_equal{}( + static_cast(std::forward<_Tp>(__t)), + static_cast(std::forward<_Up>(__u))); + else + return std::forward<_Tp>(__t) <= std::forward<_Up>(__u); +#pragma GCC diagnostic pop } template @@ -723,20 +760,20 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION typedef __is_transparent is_transparent; private: - template - static constexpr decltype(auto) - _S_cmp(_Tp&& __t, _Up&& __u, false_type) - { return std::forward<_Tp>(__t) <= std::forward<_Up>(__u); } - - template - static constexpr bool - _S_cmp(_Tp&& __t, _Up&& __u, true_type) noexcept +#if __cplusplus >= 202002L + template + static constexpr bool __ptr_cmp = requires { - return less_equal{}( - static_cast(std::forward<_Tp>(__t)), - static_cast(std::forward<_Up>(__u))); - } - + requires + ! requires + { operator<=(std::declval<_Tp>(), std::declval<_Up>()); } + && ! requires + { std::declval<_Tp>().operator<=(std::declval<_Up>()); } + && __detail::__not_overloaded_spaceship<_Tp, _Up> + && is_convertible_v<_Tp, const volatile void*> + && is_convertible_v<_Up, const volatile void*>; + }; +#else // True if there is no viable operator<= member function. template struct __not_overloaded2 : true_type { }; @@ -758,9 +795,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION : false_type { }; template - using __ptr_cmp = __and_<__not_overloaded<_Tp, _Up>, - is_convertible<_Tp, const volatile void*>, - is_convertible<_Up, const volatile void*>>; + static constexpr bool __ptr_cmp = __and_< + __not_overloaded<_Tp, _Up>, + is_convertible<_Tp, const volatile void*>, + is_convertible<_Up, const volatile void*>>::value; +#endif }; #else // < C++14 diff --git a/libstdc++-v3/include/std/concepts b/libstdc++-v3/include/std/concepts index 9c687b03e80..7673443f33a 100644 --- a/libstdc++-v3/include/std/concepts +++ b/libstdc++-v3/include/std/concepts @@ -405,6 +405,22 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION template concept strict_weak_order = relation<_Rel, _Tp, _Up>; + namespace __detail + { + // operator<=> are automatically reversed, so we need to consider + // both directions if types are different. + template + concept __not_overloaded_spaceship + = ! requires(_Tp&& __t, _Up&& __u) + { operator<=>(static_cast<_Tp&&>(__t), static_cast<_Up&&>(__u)); } + && ! requires(_Tp&& __t, _Up&& __u) + { static_cast<_Tp&&>(__t).operator<=>(static_cast<_Up&&>(__u)); } + && (is_same_v<_Tp, _Up> + || (! requires(_Tp&& __t, _Up&& __u) + { operator<=>(static_cast<_Up&&>(__u), static_cast<_Tp&&>(__t)); } + && ! requires(_Tp&& __t, _Up&& __u) + { static_cast<_Up&&>(__u).operator<=>(static_cast<_Tp&&>(__t)); })); + } _GLIBCXX_END_NAMESPACE_VERSION } // namespace #endif // __cpp_lib_concepts diff --git a/libstdc++-v3/libsupc++/compare b/libstdc++-v3/libsupc++/compare index a00bbefb069..3847d0fb141 100644 --- a/libstdc++-v3/libsupc++/compare +++ b/libstdc++-v3/libsupc++/compare @@ -560,10 +560,7 @@ namespace std _GLIBCXX_VISIBILITY(default) { static_cast<_Tp&&>(__t) <=> static_cast<_Up&&>(__u); } && convertible_to<_Tp, const volatile void*> && convertible_to<_Up, const volatile void*> - && ! requires(_Tp&& __t, _Up&& __u) - { operator<=>(static_cast<_Tp&&>(__t), static_cast<_Up&&>(__u)); } - && ! requires(_Tp&& __t, _Up&& __u) - { static_cast<_Tp&&>(__t).operator<=>(static_cast<_Up&&>(__u)); }; + && __not_overloaded_spaceship<_Tp, _Up>; } // namespace __detail // _GLIBCXX_RESOLVE_LIB_DEFECTS diff --git a/libstdc++-v3/testsuite/20_util/function_objects/comparisons_pointer_spaceship.cc b/libstdc++-v3/testsuite/20_util/function_objects/comparisons_pointer_spaceship.cc new file mode 100644 index 00000000000..80fa94b5dbf --- /dev/null +++ b/libstdc++-v3/testsuite/20_util/function_objects/comparisons_pointer_spaceship.cc @@ -0,0 +1,336 @@ +// { dg-do run { target c++20 } } + +#include +#include +#include +#include + +constexpr const char arr[] = "efgh\0abcd\0ijkl"; +constexpr const char* s1 = arr; +constexpr const char* s2 = arr+5; +constexpr const char* s3 = arr+6; + +struct CStrNone +{ + const char* str; + + constexpr + operator const char*() const + { return str; } +}; + +template +struct CStrMem +{ + const char* str; + + constexpr + operator const char*() const + { return str; } + + auto operator<=>(CStrMem const& rhs) const + { return ResultCreator::create(std::strcmp(this->str, rhs.str)); } + + auto operator<=>(const char* rhs) const + { return ResultCreator::create(std::strcmp(this->str, rhs)); } +}; + +template +struct CStrFriend +{ + const char* str; + + constexpr + operator const char*() const + { return str; } + + friend auto operator<=>(CStrFriend lhs, CStrFriend rhs) + { return ResultCreator::create(std::strcmp(lhs.str, rhs.str)); } + + friend auto operator<=>(CStrFriend lhs, const char* rhs) + { return ResultCreator::create(std::strcmp(lhs.str, rhs)); } +}; + +template +struct CStrFree +{ + const char* str; + + constexpr + operator const char*() const + { return str; } +}; + +template +auto operator<=>(CStrFree lhs, CStrFree rhs) +{ return RC::create(std::strcmp(lhs.str, rhs.str)); } + +template +auto operator<=>(CStrFree lhs, const char* rhs) +{ return RC::create(std::strcmp(lhs.str, rhs)); } + +template +struct CStrMixed +{ + const char* str; + + constexpr + operator const char*() const + { return str; } +}; + +template +auto operator<=>(CStrMixed lhs, CStrMixed rhs) +{ return RC::create(std::strcmp(lhs.str, rhs.str)); } + +template +auto operator<=>(CStrMixed lhs, CStrFree rhs) +{ return RC::create(std::strcmp(lhs.str, rhs.str)); } + +// If the type returned from shapeship does not support relational +// operators, then synthesized operators are ill-formed, SFINAEable. +struct ReturnVoid +{ + constexpr static void + create(int) { } +}; + +struct NoOperators +{ + constexpr static NoOperators + create(int) + { return NoOperators(); } +}; + +// std defined ordering types are expected +template +struct ReturnOrd +{ + constexpr static Ord + create(int cmp) + { return cmp <=> 0; } +}; + +// However, other types that provide required +// operators are supported. +struct ReturnInt +{ + constexpr static int + create(int cmp) + { return cmp; } +}; + +struct CustomOrd +{ + constexpr static CustomOrd + create(int cmp) + { return CustomOrd(cmp); } + + CustomOrd() = default; + + explicit constexpr + CustomOrd(int cmp) + : v(cmp) {} + + friend constexpr bool + operator<(CustomOrd c, std::nullptr_t) + { return c.v < 0; } + + friend constexpr bool + operator<(std::nullptr_t, CustomOrd c) + { return 0 < c.v; } + + friend constexpr bool + operator>(CustomOrd c, std::nullptr_t) + { return c.v > 0; } + + friend constexpr bool + operator>(std::nullptr_t, CustomOrd c) + { return 0 > c.v; } + + friend constexpr bool + operator<=(CustomOrd c, std::nullptr_t) + { return c.v <= 0; } + + friend constexpr bool + operator<=(std::nullptr_t, CustomOrd c) + { return 0 <= c.v; } + + friend constexpr bool + operator>=(CustomOrd c, std::nullptr_t) + { return c.v >= 0; } + + friend constexpr bool + operator>=(std::nullptr_t, CustomOrd c) + { return 0 >= c.v; } + + friend constexpr CustomOrd + operator<=>(CustomOrd c, std::nullptr_t) + { return c; } + + friend constexpr CustomOrd + operator<=>(std::nullptr_t, CustomOrd c) + { return CustomOrd(-c.v); } + +private: + int v = 0; +}; + +template +void +test_relational(bool use_overloaded) +{ + CStr1 cs1{s1}; CStr2 cs2{s2}; + + if (use_overloaded) + { + // Overloaded operaetors compare content of the string, + // and cs1 > cs2; + + VERIFY( !(cs1 < cs2) ); + VERIFY( !std::less<>{}(cs1, cs2) ); + VERIFY( !std::ranges::less{}(cs1, cs2) ); + + VERIFY( (cs1 > cs2) ); + VERIFY( std::greater<>{}(cs1, cs2) ); + VERIFY( std::ranges::greater{}(cs1, cs2) ); + + VERIFY( !(cs1 < cs2) ); + VERIFY( !std::less_equal<>{}(cs1, cs2) ); + VERIFY( !std::ranges::less_equal{}(cs1, cs2) ); + + VERIFY( (cs1 > cs2) ); + VERIFY( std::greater_equal<>{}(cs1, cs2) ); + VERIFY( std::ranges::greater_equal{}(cs1, cs2) ); + } + else + { + // Without overloaded operators, we comapre pointers, + // and cs1 < cs2; + + VERIFY( (cs1 < cs2) ); + VERIFY( std::less<>{}(cs1, cs2) ); + VERIFY( std::ranges::less{}(cs1, cs2) ); + + VERIFY( !(cs1 > cs2) ); + VERIFY( !std::greater<>{}(cs1, cs2) ); + VERIFY( !std::ranges::greater{}(cs1, cs2) ); + + VERIFY( (cs1 < cs2) ); + VERIFY( std::less_equal<>{}(cs1, cs2) ); + VERIFY( std::ranges::less_equal{}(cs1, cs2) ); + + VERIFY( !(cs1 > cs2) ); + VERIFY( !std::greater_equal<>{}(cs1, cs2) ); + VERIFY( !std::ranges::greater_equal{}(cs1, cs2) ); + } +} + +template +void +test_relational_type(bool use_overloaded) +{ + test_relational(use_overloaded); + test_relational(use_overloaded); + test_relational(use_overloaded); +} + +template +void +test_relational_return() +{ + test_relational_type>(true); + test_relational_type>(true); + test_relational_type>(true); + test_relational, CStrFree>(true); + test_relational, CStrMixed>(true); +} + +template +void +test_spaceship(bool use_overloaded) +{ + CStr1 cs1{s1}; CStr2 cs2{s2}; + + if (use_overloaded) + { + // Overloaded operaetors compare content of the string, + // and cs1 > cs2; + VERIFY( (cs1 <=> cs2) > 0 ); + VERIFY( std::compare_three_way{}(cs1, cs2) > 0 ); + } + else + { + // Without overloaded operators, we comapre pointers, + // and cs1 < cs2; + VERIFY( (cs1 <=> cs2) < 0 ); + VERIFY( std::compare_three_way{}(cs1, cs2) < 0 ); + } +} + +template +void +test_spaceship_type(bool use_overloaded) +{ + test_spaceship(use_overloaded); + test_spaceship(use_overloaded); + test_spaceship(use_overloaded); +} + +template +void +test_std_ordering() +{ + using RC = ReturnOrd; + test_relational_return(); + test_spaceship_type>(true); + test_spaceship_type>(true); + test_spaceship_type>(true); + test_spaceship, CStrFree>(true); + test_spaceship, CStrMixed>(true); +} + +template +void +test_no_relational() +{ + CStr1 c1{}; CStr2 c2{}; + static_assert(!requires { c1 < c2; }); + static_assert(!requires { c1 > c2; }); + static_assert(!requires { c1 <= c2; }); + static_assert(!requires { c1 >= c2; }); +} + +template +void +test_no_relational_type() +{ + test_no_relational(); + test_no_relational(); + test_no_relational(); +} + +template +void +test_no_relational_return() +{ + test_no_relational_type>(); + test_no_relational_type>(); + test_no_relational_type>(); + test_no_relational, CStrFree>(); + test_no_relational, CStrMixed>(); +} + +int main() +{ + test_std_ordering(); + test_std_ordering(); + test_std_ordering(); + + test_relational_type(false); + test_relational_return(); + test_relational_return(); + + test_no_relational_return(); + test_no_relational_return(); +}