diff --git a/src/catch2/catch_tostring.hpp b/src/catch2/catch_tostring.hpp index 16c4c4967b..5868ad2e68 100644 --- a/src/catch2/catch_tostring.hpp +++ b/src/catch2/catch_tostring.hpp @@ -25,6 +25,10 @@ #include #endif +#ifdef CATCH_CONFIG_CPP17_OPTIONAL +#include +#endif + #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable:4180) // We attempt to stream a function (address) by const&, which MSVC complains about but is harmless @@ -481,6 +485,16 @@ namespace Catch { template struct is_range_impl()))>> : std::true_type {}; + + // Used to exclude std::optional from the generic range StringMaker so it + // does not collide with the dedicated optional StringMaker (or a + // user-provided one). std::optional models std::ranges::range since + // C++26 (P3168), so is_range> is true and both partial + // specializations would otherwise match StringMaker>. + template struct is_optional : std::false_type {}; +#if defined(CATCH_CONFIG_CPP17_OPTIONAL) + template struct is_optional> : std::true_type {}; +#endif } // namespace Detail template @@ -516,7 +530,9 @@ namespace Catch { } template - struct StringMaker::value && !::Catch::Detail::IsStreamInsertable_v>> { + struct StringMaker::value + && !::Catch::Detail::IsStreamInsertable_v + && !::Catch::Detail::is_optional::value>> { static std::string convert( R const& range ) { return rangeToString( range ); } diff --git a/tests/SelfTest/UsageTests/ToStringOptional.tests.cpp b/tests/SelfTest/UsageTests/ToStringOptional.tests.cpp index 3671771a76..564b6eacb6 100644 --- a/tests/SelfTest/UsageTests/ToStringOptional.tests.cpp +++ b/tests/SelfTest/UsageTests/ToStringOptional.tests.cpp @@ -32,4 +32,19 @@ TEST_CASE( "std::nullopt -> toString", "[toString][optional][approvals]" ) { REQUIRE( "{ }" == ::Catch::Detail::stringify( std::nullopt ) ); } +namespace { + struct NonStreamable { int v; }; +} + +// Regression: under C++26, std::optional models std::ranges::range (P3168), so +// without the is_optional carve-out in the range StringMaker the dedicated +// optional StringMaker collides with the range one and instantiation becomes +// ambiguous. This test just needs to compile. +TEST_CASE( "std::optional -> toString does not collide with range StringMaker", + "[toString][optional][approvals]" ) { + using type = std::optional; + REQUIRE( "{ }" == ::Catch::Detail::stringify( type{} ) ); + REQUIRE( "{?}" == ::Catch::Detail::stringify( type{ NonStreamable{ 42 } } ) ); +} + #endif // CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL