From b095c0c161c8e47cad50d36256c538a13164f717 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Thu, 29 Jan 2026 08:42:17 -0500 Subject: [PATCH 1/4] Add reproducer test set --- test/Jamfile.v2 | 1 + test/writing-test-ts/github_issue_475.cpp | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 test/writing-test-ts/github_issue_475.cpp 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); +} From 42db56200992946d336a298ce0b81826f6094b12 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Thu, 29 Jan 2026 09:38:55 -0500 Subject: [PATCH 2/4] Add base class to allow for friend functions with std::optional --- include/boost/test/tools/assertion.hpp | 153 +++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 9 deletions(-) diff --git a/include/boost/test/tools/assertion.hpp b/include/boost/test/tools/assertion.hpp index 39eab3b03b..d350707563 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) && \ + (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) +#include +#endif + #include //____________________________________________________________________________// @@ -38,6 +44,26 @@ 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) && \ + (__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 +103,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 +117,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 +213,93 @@ 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) && \ + (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) +#ifndef BOOST_NO_CXX11_RVALUE_REFERENCES + +// 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 // BOOST_NO_CXX11_RVALUE_REFERENCES +template +struct optional_friends_base {}; +#endif + +#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) && \ + (__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 +353,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 From be5918db08947b5239382915640e679ee4743380 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Thu, 29 Jan 2026 09:43:41 -0500 Subject: [PATCH 3/4] Remove unnecessary macros --- include/boost/test/tools/assertion.hpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/include/boost/test/tools/assertion.hpp b/include/boost/test/tools/assertion.hpp index d350707563..ad424cead5 100644 --- a/include/boost/test/tools/assertion.hpp +++ b/include/boost/test/tools/assertion.hpp @@ -224,7 +224,6 @@ template class binary_expr; #if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10) && \ (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) -#ifndef BOOST_NO_CXX11_RVALUE_REFERENCES // Primary template - empty (for non-optional types) template::value> @@ -276,11 +275,6 @@ struct optional_friends_base { } }; -#else // BOOST_NO_CXX11_RVALUE_REFERENCES -template -struct optional_friends_base {}; -#endif - #else // Not GCC < 10 template struct optional_friends_base {}; From 3efd8970899218b96cc37a1ff7fce13e206284da Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Thu, 29 Jan 2026 14:51:29 -0500 Subject: [PATCH 4/4] Apply same workaround for clangs < 10 --- include/boost/test/tools/assertion.hpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/include/boost/test/tools/assertion.hpp b/include/boost/test/tools/assertion.hpp index ad424cead5..472b60e221 100644 --- a/include/boost/test/tools/assertion.hpp +++ b/include/boost/test/tools/assertion.hpp @@ -31,7 +31,7 @@ #endif // GCC < 10 workaround for std::optional comparison ambiguity (see below) -#if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10) && \ +#if ((defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10)) || (defined(__clang__) && __clang_major__ < 11)) && \ (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) #include #endif @@ -52,8 +52,9 @@ namespace assertion { template struct is_std_optional : std::false_type {}; -#if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10) && \ +#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 @@ -62,6 +63,7 @@ template struct is_std_optional const&> : std::true_type {}; template struct is_std_optional&&> : std::true_type {}; + #endif // ************************************************************************** // @@ -222,7 +224,7 @@ BOOST_TEST_FOR_EACH_CONST_OP( DEFINE_CONST_OPER ) template class binary_expr; -#if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 10) && \ +#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) @@ -311,7 +313,7 @@ class expression_base : public optional_friends_base { // 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) && \ +#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