diff --git a/include/boost/test/tools/assertion.hpp b/include/boost/test/tools/assertion.hpp index 39eab3b03b..472b60e221 100644 --- a/include/boost/test/tools/assertion.hpp +++ b/include/boost/test/tools/assertion.hpp @@ -30,6 +30,12 @@ #include #endif +// GCC < 10 workaround for std::optional comparison ambiguity (see below) +#if ((defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10)) || (defined(__clang__) && __clang_major__ < 11)) && \ + (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) +#include +#endif + #include //____________________________________________________________________________// @@ -38,6 +44,28 @@ namespace boost { namespace test_tools { namespace assertion { +// ************************************************************************** // +// ************** assertion::is_std_optional ************** // +// ************************************************************************** // +// Trait to detect std::optional. Used for GCC < 10 workaround. + +template +struct is_std_optional : std::false_type {}; + +#if ((defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10)) || (defined(__clang__) && __clang_major__ < 11)) && \ + (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) + +template +struct is_std_optional> : std::true_type {}; +template +struct is_std_optional&> : std::true_type {}; +template +struct is_std_optional const&> : std::true_type {}; +template +struct is_std_optional&&> : std::true_type {}; + +#endif + // ************************************************************************** // // ************** assertion::operators ************** // // ************************************************************************** // @@ -77,7 +105,8 @@ namespace op { #ifndef BOOST_NO_CXX11_DECLTYPE -#define BOOST_TEST_FOR_EACH_CONST_OP(action)\ +// Non-comparison operators (never need SFINAE workaround) +#define BOOST_TEST_FOR_EACH_NONCOMP_OP(action)\ action(->*, MEMP, ->*, MEMP ) \ \ action( * , MUL , * , MUL ) \ @@ -90,15 +119,20 @@ namespace op { action( <<, LSH , << , LSH ) \ action( >>, RSH , >> , RSH ) \ \ - BOOST_TEST_FOR_EACH_COMP_OP(action) \ - \ action( & , BAND, & , BAND ) \ action( ^ , XOR , ^ , XOR ) \ action( | , BOR , | , BOR ) \ /**/ +#define BOOST_TEST_FOR_EACH_CONST_OP(action)\ + BOOST_TEST_FOR_EACH_NONCOMP_OP(action) \ + BOOST_TEST_FOR_EACH_COMP_OP(action) \ +/**/ + #else +#define BOOST_TEST_FOR_EACH_NONCOMP_OP(action) + #define BOOST_TEST_FOR_EACH_CONST_OP(action)\ BOOST_TEST_FOR_EACH_COMP_OP(action) \ /**/ @@ -181,20 +215,87 @@ BOOST_TEST_FOR_EACH_CONST_OP( DEFINE_CONST_OPER ) } // namespace op // ************************************************************************** // -// ************** assertion::expression_base ************** // +// ************** assertion::optional_friends_base ************** // // ************************************************************************** // -// Defines expression operators +// GCC < 10 workaround: Base class that conditionally provides hidden friend +// comparison operators only when ValType is std::optional. Hidden friends are +// found via ADL and as non-templates beat std::optional's template operators. +// See: https://github.com/boostorg/test/issues/475 template class binary_expr; +#if ((defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10)) || (defined(__clang__) && __clang_major__ < 11)) && \ + (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) + +// Primary template - empty (for non-optional types) +template::value> +struct optional_friends_base {}; + +// Specialization for std::optional types - provides hidden friend operators +template +struct optional_friends_base { + friend binary_expr> + operator==( ExprType lhs, ValType rhs ) + { + return binary_expr>( + std::move(lhs), std::move(rhs)); + } + + friend binary_expr> + operator!=( ExprType lhs, ValType rhs ) + { + return binary_expr>( + std::move(lhs), std::move(rhs)); + } + + friend binary_expr> + operator<( ExprType lhs, ValType rhs ) + { + return binary_expr>( + std::move(lhs), std::move(rhs)); + } + + friend binary_expr> + operator<=( ExprType lhs, ValType rhs ) + { + return binary_expr>( + std::move(lhs), std::move(rhs)); + } + + friend binary_expr> + operator>( ExprType lhs, ValType rhs ) + { + return binary_expr>( + std::move(lhs), std::move(rhs)); + } + + friend binary_expr> + operator>=( ExprType lhs, ValType rhs ) + { + return binary_expr>( + std::move(lhs), std::move(rhs)); + } +}; + +#else // Not GCC < 10 +template +struct optional_friends_base {}; +#endif // GCC < 10 && C++17 + +// ************************************************************************** // +// ************** assertion::expression_base ************** // +// ************************************************************************** // +// Defines expression operators + template -class expression_base { +class expression_base : public optional_friends_base { public: #ifndef BOOST_NO_CXX11_RVALUE_REFERENCES template struct RhsT : remove_const::type> {}; - + +// Regular operator support (non-comparison operators) #define ADD_OP_SUPPORT( oper, name, _, _i ) \ template \ binary_expr(rhs) ); \ } \ /**/ + +// GCC < 10 workaround: comparison operators with SFINAE to exclude std::optional +// when ValType is also std::optional. The hidden friend operators in the base class +// optional_friends_base will handle the optional==optional case instead. +#if ((defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10)) || (defined(__clang__) && __clang_major__ < 11)) && \ + (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) +#define ADD_OP_SUPPORT_COMP( oper, name, _, _i ) \ + template::value || \ + !is_std_optional::type>::value, int>::type = 0> \ + binary_expr::type> > \ + operator oper( T&& rhs ) \ + { \ + return binary_expr::type> > \ + ( std::forward( \ + *static_cast(this) ), \ + std::forward(rhs) ); \ + } \ +/**/ #else +#define ADD_OP_SUPPORT_COMP ADD_OP_SUPPORT +#endif + +#else // BOOST_NO_CXX11_RVALUE_REFERENCES #define ADD_OP_SUPPORT( oper, name, _, _i ) \ template \ @@ -222,10 +349,14 @@ class expression_base { rhs ); \ } \ /**/ -#endif +#define ADD_OP_SUPPORT_COMP ADD_OP_SUPPORT + +#endif // BOOST_NO_CXX11_RVALUE_REFERENCES - BOOST_TEST_FOR_EACH_CONST_OP( ADD_OP_SUPPORT ) + BOOST_TEST_FOR_EACH_NONCOMP_OP( ADD_OP_SUPPORT ) + BOOST_TEST_FOR_EACH_COMP_OP( ADD_OP_SUPPORT_COMP ) #undef ADD_OP_SUPPORT + #undef ADD_OP_SUPPORT_COMP #ifndef BOOST_NO_CXX11_AUTO_DECLARATIONS // Disabled operators diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 33c2bcdfb3..9e8ca58a7c 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -188,6 +188,7 @@ test-suite "writing-test-ts" # [ boost.test-self-test run-fail : writing-test-ts : test-timeout-fail : : : : : : [ requires cxx11_hdr_thread cxx11_hdr_chrono ] ] # [ boost.test-self-test run : writing-test-ts : test-timeout-suite : : : : : : $(requirements_datasets) [ requires cxx11_hdr_thread cxx11_hdr_chrono ] ] # [ boost.test-self-test run-fail : writing-test-ts : test-timeout-suite-fail : : : : : : $(requirements_datasets) [ requires cxx11_hdr_thread cxx11_hdr_chrono ] ] + [ boost.test-self-test run : writing-test-ts : github_issue_475 : : : : : : [ requires cxx17_hdr_optional ] ] ; #_________________________________________________________________________________________________# diff --git a/test/writing-test-ts/github_issue_475.cpp b/test/writing-test-ts/github_issue_475.cpp new file mode 100644 index 0000000000..5959db92bd --- /dev/null +++ b/test/writing-test-ts/github_issue_475.cpp @@ -0,0 +1,15 @@ +// Copyright 2025 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#define BOOST_TEST_MODULE github_issue_475 +#include +#include + +BOOST_TEST_DONT_PRINT_LOG_VALUE(std::optional) + +BOOST_AUTO_TEST_CASE(test1) +{ + std::optional a,b; + BOOST_TEST(a==b); +}