// Copyright Catch2 Authors // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) // SPDX-License-Identifier: BSL-1.0 #pragma once #include #include #include #include #include #include #include #include /** \file * Why does decomposing look the way it does: * * Conceptually, decomposing is simple. We change `REQUIRE( a == b )` into * `Decomposer{} <= a == b`, so that `Decomposer{} <= a` is evaluated first, * and our custom operator is used for `a == b`, because `a` is transformed * into `ExprLhs` and then into `BinaryExpr`. * * In practice, decomposing ends up a mess, because we have to support * various fun things. * * 1) Types that are only comparable with literal 0, and they do this by * comparing against a magic type with pointer constructor and deleted * other constructors. Example: `REQUIRE((a <=> b) == 0)` in libstdc++ * * 2) Types that are only comparable with literal 0, and they do this by * comparing against a magic type with consteval integer constructor. * Example: `REQUIRE((a <=> b) == 0)` in current MSVC STL. * * 3) Types that have no linkage, and so we cannot form a reference to * them. Example: some implementations of traits. * * 4) Starting with C++20, when the compiler sees `a == b`, it also uses * `b == a` when constructing the overload set. For us this means that * when the compiler handles `ExprLhs == b`, it also tries to resolve * the overload set for `b == ExprLhs`. * * To accommodate these use cases, decomposer ended up rather complex. * * 1) These types are handled by adding SFINAE overloads to our comparison * operators, checking whether `T == U` are comparable with the given * operator, and if not, whether T (or U) are comparable with literal 0. * If yes, the overload compares T (or U) with 0 literal inline in the * definition. * * Note that for extra correctness, we check that the other type is * either an `int` (literal 0 is captured as `int` by templates), or * a `long` (some platforms use 0L for `NULL` and we want to support * that for pointer comparisons). * * 2) For these types, `is_foo_comparable` is true, but letting * them fall into the overload that actually does `T == int` causes * compilation error. Handling them requires that the decomposition * is `constexpr`, so that P2564R3 applies and the `consteval` from * their accompanying magic type is propagated through the `constexpr` * call stack. * * However this is not enough to handle these types automatically, * because our default is to capture types by reference, to avoid * runtime copies. While these references cannot become dangling, * they outlive the constexpr context and thus the default capture * path cannot be actually constexpr. * * The solution is to capture these types by value, by explicitly * specializing `Catch::capture_by_value` for them. Catch2 provides * specialization for `std::foo_ordering`s, but users can specialize * the trait for their own types as well. * * 3) If a type has no linkage, we also cannot capture it by reference. * The solution is once again to capture them by value. We handle * the common cases by using `std::is_arithmetic` and `std::is_enum` * as the default for `Catch::capture_by_value`, but that is only a * some-effort heuristic. These combine to capture all possible bitfield * bases, and also some trait-like types. As with 2), users can * specialize `capture_by_value` for their own types as needed. * * 4) To support C++20 and make the SFINAE on our decomposing operators * work, the SFINAE has to happen in return type, rather than in * a template type. This is due to our use of logical type traits * (`conjunction`/`disjunction`/`negation`), that we use to workaround * an issue in older (9-) versions of GCC. I still blame C++20 for * this, because without the comparison order switching, the logical * traits could still be used in template type. * * There are also other side concerns, e.g. supporting both `REQUIRE(a)` * and `REQUIRE(a == b)`, or making `REQUIRE_THAT(a, IsEqual(b))` slot * nicely into the same expression handling logic, but these are rather * straightforward and add only a bit of complexity (e.g. common base * class for decomposed expressions). */ #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable:4389) // '==' : signed/unsigned mismatch #pragma warning(disable:4018) // more "signed/unsigned mismatch" #pragma warning(disable:4312) // Converting int to T* using reinterpret_cast (issue on x64 platform) #pragma warning(disable:4180) // qualifier applied to function type has no meaning #pragma warning(disable:4800) // Forcing result to true or false #endif #ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wsign-compare" # pragma clang diagnostic ignored "-Wnon-virtual-dtor" #elif defined __GNUC__ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wsign-compare" # pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #endif #if defined(CATCH_CPP20_OR_GREATER) && __has_include() # include # if defined( __cpp_lib_three_way_comparison ) && \ __cpp_lib_three_way_comparison >= 201907L # define CATCH_CONFIG_CPP20_COMPARE_OVERLOADS # endif #endif namespace Catch { namespace Detail { // This was added in C++20, but we require only C++17. template using RemoveCVRef_t = std::remove_cv_t>; } // Note: This is about as much as we can currently reasonably support. // In an ideal world, we could capture by value small trivially // copyable types, but the actual `std::is_trivially_copyable` // trait is a huge mess with standard-violating results on // GCC and Clang, which are unlikely to be fixed soon due to ABI // concerns. // `std::is_scalar` also causes issues due to the `is_pointer` // component, which causes ambiguity issues with (references-to) // function pointer. If those are resolved, we still need to // disambiguate the overload set for arrays, through explicit // overload for references to sized arrays. template struct capture_by_value : std::integral_constant::value || std::is_enum::value> {}; #if defined( CATCH_CONFIG_CPP20_COMPARE_OVERLOADS ) template <> struct capture_by_value : std::true_type {}; template <> struct capture_by_value : std::true_type {}; template <> struct capture_by_value : std::true_type {}; #endif template struct always_false : std::false_type {}; class ITransientExpression { //! Type-erased streaming function for the derived expression using StreamFn = void ( * )( void const*, std::ostream& ); StreamFn m_streamFn; bool m_isBinaryExpression; bool m_result; public: constexpr auto isBinaryExpression() const -> bool { return m_isBinaryExpression; } constexpr auto getResult() const -> bool { return m_result; } constexpr ITransientExpression( StreamFn streamFn, bool isBinaryExpression, bool result ): m_streamFn( streamFn ), m_isBinaryExpression( isBinaryExpression ), m_result( result ) {} friend std::ostream& operator<<( std::ostream& out, ITransientExpression const& expr ) { expr.m_streamFn( &expr, out ); return out; } }; void formatReconstructedExpression( std::ostream &os, std::string const& lhs, StringRef op, std::string const& rhs ); template class BinaryExpr final : public ITransientExpression { LhsT m_lhs; StringRef m_op; RhsT m_rhs; static void streamFn( void const* self, std::ostream& out ) { auto const& expr = *static_cast( static_cast( self ) ); formatReconstructedExpression( out, Catch::Detail::stringify( expr.m_lhs ), expr.m_op, Catch::Detail::stringify( expr.m_rhs ) ); } public: constexpr BinaryExpr( bool comparisonResult, LhsT lhs, StringRef op, RhsT rhs ) : ITransientExpression{ &streamFn, true, comparisonResult }, m_lhs( lhs ), m_op( op ), m_rhs( rhs ) {} }; #define CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( op ) \ template \ auto operator op( BinaryExpr const&, T ) \ ->BinaryExpr const { \ static_assert( always_false::value, \ "chained comparisons are not supported inside " \ "assertions, wrap the expression inside " \ "parentheses, or decompose it" ); \ } /* * The operators are **intentionally** not hidden friends. * * If they were hidden friends, all of them would get stamped out when * a `BinaryExpr` is instantiated with a new type. In practice, only * few operators are used for each different `BinaryExpr`, * so this avoids bunch of wasted work. */ CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( && ) CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( || ) CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( == ) CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( != ) CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( > ) CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( < ) CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( >= ) CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR( <= ) #undef CATCH_INTERNAL_DEFINE_BINARY_EXPR_OPERATOR template class UnaryExpr final : public ITransientExpression { LhsT m_lhs; static void streamFn( void const* self, std::ostream& os ) { auto const& expr = *static_cast( static_cast( self ) ); os << Catch::Detail::stringify( expr.m_lhs ); } public: explicit constexpr UnaryExpr( LhsT lhs ) : ITransientExpression{ &streamFn, false, static_cast(lhs) }, m_lhs( lhs ) {} }; template class ExprLhs { LhsT m_lhs; public: explicit constexpr ExprLhs( LhsT lhs ) : m_lhs( lhs ) {} // We rely on reference collapsing if LhsT is already a reference. // Used in the out-of-class decomposition operators constexpr LhsT& getLhs() { return m_lhs; } constexpr auto makeUnaryExpr() const -> UnaryExpr { return UnaryExpr{ m_lhs }; } }; /* * The comparison operators are **intentionally** not hidden friends. * * If they were hidden friends, all of them would get stamped out when * a `ExprLhs` is instantiated with a new type. In practice, only few * operators are used for each different `ExprLhs`, so this avoids * a lot of wasted work. */ #define CATCH_INTERNAL_DEFINE_EXPRESSION_EQUALITY_OPERATOR( id, op ) \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \ ->std::enable_if_t< \ Detail::conjunction, \ Detail::negation>>>::value, \ BinaryExpr> { \ return { static_cast( lhs.getLhs() op rhs ), \ lhs.getLhs(), \ #op##_sr, \ rhs }; \ } \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT rhs ) \ ->std::enable_if_t< \ Detail::conjunction, \ capture_by_value>::value, \ BinaryExpr> { \ return { static_cast( lhs.getLhs() op rhs ), \ lhs.getLhs(), \ #op##_sr, \ rhs }; \ } \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT rhs ) \ ->std::enable_if_t< \ Detail::conjunction< \ Detail::negation>, \ Detail::is_eq_0_comparable, /* We allow long because we \ want `ptr op NULL` to be \ accepted */ \ Detail::disjunction, \ std::is_same>>::value, \ BinaryExpr> { \ if ( rhs != 0 ) { throw_test_failure_exception(); } \ return { static_cast( lhs.getLhs() op 0 ), \ lhs.getLhs(), \ #op##_sr, \ rhs }; \ } \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT rhs ) \ ->std::enable_if_t< \ Detail::conjunction< \ Detail::negation>, \ Detail::is_eq_0_comparable, /* We allow long because we \ want `ptr op NULL` to be \ accepted */ \ Detail::disjunction, \ std::is_same>>::value, \ BinaryExpr> { \ if ( lhs.getLhs() != 0 ) { throw_test_failure_exception(); } \ return { static_cast( 0 op rhs ), lhs.getLhs(), #op##_sr, rhs }; \ } CATCH_INTERNAL_DEFINE_EXPRESSION_EQUALITY_OPERATOR( eq, == ) CATCH_INTERNAL_DEFINE_EXPRESSION_EQUALITY_OPERATOR( ne, != ) #undef CATCH_INTERNAL_DEFINE_EXPRESSION_EQUALITY_OPERATOR #define CATCH_INTERNAL_DEFINE_EXPRESSION_COMPARISON_OPERATOR( id, op ) \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \ ->std::enable_if_t< \ Detail::conjunction, \ Detail::negation>>>::value, \ BinaryExpr> { \ return { static_cast( lhs.getLhs() op rhs ), \ lhs.getLhs(), \ #op##_sr, \ rhs }; \ } \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT rhs ) \ ->std::enable_if_t< \ Detail::conjunction, \ capture_by_value>::value, \ BinaryExpr> { \ return { static_cast( lhs.getLhs() op rhs ), \ lhs.getLhs(), \ #op##_sr, \ rhs }; \ } \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT rhs ) \ ->std::enable_if_t< \ Detail::conjunction< \ Detail::negation>, \ Detail::is_##id##_0_comparable, \ std::is_same>::value, \ BinaryExpr> { \ if ( rhs != 0 ) { throw_test_failure_exception(); } \ return { static_cast( lhs.getLhs() op 0 ), \ lhs.getLhs(), \ #op##_sr, \ rhs }; \ } \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT rhs ) \ ->std::enable_if_t< \ Detail::conjunction< \ Detail::negation>, \ Detail::is_##id##_0_comparable, \ std::is_same>::value, \ BinaryExpr> { \ if ( lhs.getLhs() != 0 ) { throw_test_failure_exception(); } \ return { static_cast( 0 op rhs ), lhs.getLhs(), #op##_sr, rhs }; \ } CATCH_INTERNAL_DEFINE_EXPRESSION_COMPARISON_OPERATOR( lt, < ) CATCH_INTERNAL_DEFINE_EXPRESSION_COMPARISON_OPERATOR( le, <= ) CATCH_INTERNAL_DEFINE_EXPRESSION_COMPARISON_OPERATOR( gt, > ) CATCH_INTERNAL_DEFINE_EXPRESSION_COMPARISON_OPERATOR( ge, >= ) #undef CATCH_INTERNAL_DEFINE_EXPRESSION_COMPARISON_OPERATOR #define CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR( op ) \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \ ->std::enable_if_t< \ !capture_by_value>::value, \ BinaryExpr> { \ return { static_cast( lhs.getLhs() op rhs ), \ lhs.getLhs(), \ #op##_sr, \ rhs }; \ } \ template \ constexpr auto operator op( ExprLhs&& lhs, RhsT rhs ) \ ->std::enable_if_t::value, \ BinaryExpr> { \ return { static_cast( lhs.getLhs() op rhs ), \ lhs.getLhs(), \ #op##_sr, \ rhs }; \ } CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR( | ) CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR( & ) CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR( ^) #undef CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR template auto operator&&( ExprLhs&&, RhsT&& ) -> BinaryExpr { static_assert( always_false::value, "operator&& is not supported inside assertions, " "wrap the expression inside parentheses, or decompose it" ); } template auto operator||( ExprLhs&&, RhsT&& ) -> BinaryExpr { static_assert( always_false::value, "operator|| is not supported inside assertions, " "wrap the expression inside parentheses, or decompose it" ); } struct Decomposer { template >::value, int> = 0> constexpr friend auto operator <= ( Decomposer &&, T && lhs ) -> ExprLhs { return ExprLhs{ lhs }; } template ::value, int> = 0> constexpr friend auto operator <= ( Decomposer &&, T value ) -> ExprLhs { return ExprLhs{ value }; } }; } // end namespace Catch #ifdef _MSC_VER #pragma warning(pop) #endif #ifdef __clang__ # pragma clang diagnostic pop #elif defined __GNUC__ # pragma GCC diagnostic pop #endif