Skip to content

Commit dc51386

Browse files
committed
Support literal-zero detectors using consteval int constructors
This was originally motivated by `REQUIRE((a <=> b) == 0)` no longer compiling using MSVC. After some investigation, I found that they changed their implementation of the zero literal detector from the previous pointer-constructor with deleted other constructors, into one that uses `consteval` constructor from int. This breaks the previous detection logic, because now `is_foo_comparable<std::strong_ordering, int>` is true, but actually trying to compare them is a compile-time error... The solution was to make the decomposition `constexpr` and rely on a late C++20 DR that makes it so that `consteval` propagates up through the callstack of `constexpr` functions, until it either runs out of `constexpr` functions, or succeeds. However, the default handling of types in decomposition is to take a reference to them. This reference never becomes dangling, but because the constexpr evaluation engine cannot prove this, decomposition paths taking references to objects cannot be actually evaluated at compilation time. Thankfully we already did have a value-oriented decomposition path for arithmetic types (as these are common linkage-less types), so we could just explicitly spell out the `std::foo_ordering` types as also being supposed to be decomposed by-value. Two more fun facts about these changes 1) The original motivation of the MSVC change was to avoid trigering a `Wzero-as-null-pointer-constant` warning. I still do not believe this was a good decision. 2) Current latest version of MSVC does not actually implement the aforementioned C++20 DR, so even with this commit, MSVC cannot compile `REQUIRE((a <=> b) == 0)`.
1 parent bbba3d8 commit dc51386

File tree

5 files changed

+154
-41
lines changed

5 files changed

+154
-41
lines changed

src/catch2/internal/catch_compiler_capabilities.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
# define CATCH_CPP17_OR_GREATER
3838
# endif
3939

40+
# if (__cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)
41+
# define CATCH_CPP20_OR_GREATER
42+
# endif
43+
4044
#endif
4145

4246
// Only GCC compiler should be used in this block, so other compilers trying to

src/catch2/internal/catch_decomposer.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010

1111
namespace Catch {
1212

13-
ITransientExpression::~ITransientExpression() = default;
13+
void ITransientExpression::streamReconstructedExpression(
14+
std::ostream& os ) const {
15+
// We can't make this function pure virtual to keep ITransientExpression
16+
// constexpr, so we write error message instead
17+
os << "Some class derived from ITransientExpression without overriding streamReconstructedExpression";
18+
}
1419

1520
void formatReconstructedExpression( std::ostream &os, std::string const& lhs, StringRef op, std::string const& rhs ) {
1621
if( lhs.size() + rhs.size() < 40 &&

src/catch2/internal/catch_decomposer.hpp

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <catch2/internal/catch_compare_traits.hpp>
1414
#include <catch2/internal/catch_test_failure_exception.hpp>
1515
#include <catch2/internal/catch_logical_traits.hpp>
16+
#include <catch2/internal/catch_compiler_capabilities.hpp>
1617

1718
#include <type_traits>
1819
#include <iosfwd>
@@ -34,8 +35,33 @@
3435
# pragma GCC diagnostic ignored "-Wsign-compare"
3536
#endif
3637

38+
#if defined(CATCH_CPP20_OR_GREATER) && __has_include(<compare>)
39+
# include <compare>
40+
# if defined( __cpp_lib_three_way_comparison ) && \
41+
__cpp_lib_three_way_comparison >= 201907L
42+
# define CATCH_CONFIG_CPP20_COMPARE_OVERLOADS
43+
# endif
44+
#endif
45+
3746
namespace Catch {
3847

48+
// Note: There is nothing that stops us from extending this,
49+
// e.g. to `std::is_scalar`, but the more encompassing
50+
// traits are usually also more expensive. For now we
51+
// keep this as it used to be and it can be changed later.
52+
template <typename T>
53+
struct capture_by_value
54+
: std::integral_constant<bool, std::is_arithmetic<T>{}> {};
55+
56+
#if defined( CATCH_CONFIG_CPP20_COMPARE_OVERLOADS )
57+
template <>
58+
struct capture_by_value<std::strong_ordering> : std::true_type {};
59+
template <>
60+
struct capture_by_value<std::weak_ordering> : std::true_type {};
61+
template <>
62+
struct capture_by_value<std::partial_ordering> : std::true_type {};
63+
#endif
64+
3965
template <typename T>
4066
struct always_false : std::false_type {};
4167

@@ -44,11 +70,12 @@ namespace Catch {
4470
bool m_result;
4571

4672
public:
47-
auto isBinaryExpression() const -> bool { return m_isBinaryExpression; }
48-
auto getResult() const -> bool { return m_result; }
49-
virtual void streamReconstructedExpression( std::ostream &os ) const = 0;
73+
constexpr auto isBinaryExpression() const -> bool { return m_isBinaryExpression; }
74+
constexpr auto getResult() const -> bool { return m_result; }
75+
//! This function **has** to be overriden by the derived class.
76+
virtual void streamReconstructedExpression( std::ostream& os ) const;
5077

51-
ITransientExpression( bool isBinaryExpression, bool result )
78+
constexpr ITransientExpression( bool isBinaryExpression, bool result )
5279
: m_isBinaryExpression( isBinaryExpression ),
5380
m_result( result )
5481
{}
@@ -59,7 +86,7 @@ namespace Catch {
5986

6087
// We don't actually need a virtual destructor, but many static analysers
6188
// complain if it's not here :-(
62-
virtual ~ITransientExpression(); // = default;
89+
virtual ~ITransientExpression() = default;
6390

6491
friend std::ostream& operator<<(std::ostream& out, ITransientExpression const& expr) {
6592
expr.streamReconstructedExpression(out);
@@ -81,7 +108,7 @@ namespace Catch {
81108
}
82109

83110
public:
84-
BinaryExpr( bool comparisonResult, LhsT lhs, StringRef op, RhsT rhs )
111+
constexpr BinaryExpr( bool comparisonResult, LhsT lhs, StringRef op, RhsT rhs )
85112
: ITransientExpression{ true, comparisonResult },
86113
m_lhs( lhs ),
87114
m_op( op ),
@@ -154,7 +181,7 @@ namespace Catch {
154181
}
155182

156183
public:
157-
explicit UnaryExpr( LhsT lhs )
184+
explicit constexpr UnaryExpr( LhsT lhs )
158185
: ITransientExpression{ false, static_cast<bool>(lhs) },
159186
m_lhs( lhs )
160187
{}
@@ -165,30 +192,30 @@ namespace Catch {
165192
class ExprLhs {
166193
LhsT m_lhs;
167194
public:
168-
explicit ExprLhs( LhsT lhs ) : m_lhs( lhs ) {}
195+
explicit constexpr ExprLhs( LhsT lhs ) : m_lhs( lhs ) {}
169196

170197
#define CATCH_INTERNAL_DEFINE_EXPRESSION_EQUALITY_OPERATOR( id, op ) \
171198
template <typename RhsT> \
172-
friend auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \
199+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \
173200
->std::enable_if_t< \
174201
Detail::conjunction<Detail::is_##id##_comparable<LhsT, RhsT>, \
175-
Detail::negation<std::is_arithmetic< \
202+
Detail::negation<capture_by_value< \
176203
std::remove_reference_t<RhsT>>>>::value, \
177204
BinaryExpr<LhsT, RhsT const&>> { \
178205
return { \
179206
static_cast<bool>( lhs.m_lhs op rhs ), lhs.m_lhs, #op##_sr, rhs }; \
180207
} \
181208
template <typename RhsT> \
182-
friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
209+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
183210
->std::enable_if_t< \
184211
Detail::conjunction<Detail::is_##id##_comparable<LhsT, RhsT>, \
185-
std::is_arithmetic<RhsT>>::value, \
212+
capture_by_value<RhsT>>::value, \
186213
BinaryExpr<LhsT, RhsT>> { \
187214
return { \
188215
static_cast<bool>( lhs.m_lhs op rhs ), lhs.m_lhs, #op##_sr, rhs }; \
189216
} \
190217
template <typename RhsT> \
191-
friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
218+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
192219
->std::enable_if_t< \
193220
Detail::conjunction< \
194221
Detail::negation<Detail::is_##id##_comparable<LhsT, RhsT>>, \
@@ -202,7 +229,7 @@ namespace Catch {
202229
static_cast<bool>( lhs.m_lhs op 0 ), lhs.m_lhs, #op##_sr, rhs }; \
203230
} \
204231
template <typename RhsT> \
205-
friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
232+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
206233
->std::enable_if_t< \
207234
Detail::conjunction< \
208235
Detail::negation<Detail::is_##id##_comparable<LhsT, RhsT>>, \
@@ -220,28 +247,29 @@ namespace Catch {
220247

221248
#undef CATCH_INTERNAL_DEFINE_EXPRESSION_EQUALITY_OPERATOR
222249

250+
223251
#define CATCH_INTERNAL_DEFINE_EXPRESSION_COMPARISON_OPERATOR( id, op ) \
224252
template <typename RhsT> \
225-
friend auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \
253+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \
226254
->std::enable_if_t< \
227255
Detail::conjunction<Detail::is_##id##_comparable<LhsT, RhsT>, \
228-
Detail::negation<std::is_arithmetic< \
256+
Detail::negation<capture_by_value< \
229257
std::remove_reference_t<RhsT>>>>::value, \
230258
BinaryExpr<LhsT, RhsT const&>> { \
231259
return { \
232260
static_cast<bool>( lhs.m_lhs op rhs ), lhs.m_lhs, #op##_sr, rhs }; \
233261
} \
234262
template <typename RhsT> \
235-
friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
263+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
236264
->std::enable_if_t< \
237265
Detail::conjunction<Detail::is_##id##_comparable<LhsT, RhsT>, \
238-
std::is_arithmetic<RhsT>>::value, \
266+
capture_by_value<RhsT>>::value, \
239267
BinaryExpr<LhsT, RhsT>> { \
240268
return { \
241269
static_cast<bool>( lhs.m_lhs op rhs ), lhs.m_lhs, #op##_sr, rhs }; \
242270
} \
243271
template <typename RhsT> \
244-
friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
272+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
245273
->std::enable_if_t< \
246274
Detail::conjunction< \
247275
Detail::negation<Detail::is_##id##_comparable<LhsT, RhsT>>, \
@@ -253,7 +281,7 @@ namespace Catch {
253281
static_cast<bool>( lhs.m_lhs op 0 ), lhs.m_lhs, #op##_sr, rhs }; \
254282
} \
255283
template <typename RhsT> \
256-
friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
284+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
257285
->std::enable_if_t< \
258286
Detail::conjunction< \
259287
Detail::negation<Detail::is_##id##_comparable<LhsT, RhsT>>, \
@@ -274,16 +302,16 @@ namespace Catch {
274302

275303
#define CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR( op ) \
276304
template <typename RhsT> \
277-
friend auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \
305+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT&& rhs ) \
278306
->std::enable_if_t< \
279-
!std::is_arithmetic<std::remove_reference_t<RhsT>>::value, \
307+
!capture_by_value<std::remove_reference_t<RhsT>>::value, \
280308
BinaryExpr<LhsT, RhsT const&>> { \
281309
return { \
282310
static_cast<bool>( lhs.m_lhs op rhs ), lhs.m_lhs, #op##_sr, rhs }; \
283311
} \
284312
template <typename RhsT> \
285-
friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
286-
->std::enable_if_t<std::is_arithmetic<RhsT>::value, \
313+
constexpr friend auto operator op( ExprLhs&& lhs, RhsT rhs ) \
314+
->std::enable_if_t<capture_by_value<RhsT>::value, \
287315
BinaryExpr<LhsT, RhsT>> { \
288316
return { \
289317
static_cast<bool>( lhs.m_lhs op rhs ), lhs.m_lhs, #op##_sr, rhs }; \
@@ -309,19 +337,23 @@ namespace Catch {
309337
"wrap the expression inside parentheses, or decompose it");
310338
}
311339

312-
auto makeUnaryExpr() const -> UnaryExpr<LhsT> {
340+
constexpr auto makeUnaryExpr() const -> UnaryExpr<LhsT> {
313341
return UnaryExpr<LhsT>{ m_lhs };
314342
}
315343
};
316344

317345
struct Decomposer {
318-
template<typename T, std::enable_if_t<!std::is_arithmetic<std::remove_reference_t<T>>::value, int> = 0>
319-
friend auto operator <= ( Decomposer &&, T && lhs ) -> ExprLhs<T const&> {
346+
template <typename T,
347+
std::enable_if_t<
348+
!capture_by_value<std::remove_reference_t<T>>::value,
349+
int> = 0>
350+
constexpr friend auto operator <= ( Decomposer &&, T && lhs ) -> ExprLhs<T const&> {
320351
return ExprLhs<const T&>{ lhs };
321352
}
322353

323-
template<typename T, std::enable_if_t<std::is_arithmetic<T>::value, int> = 0>
324-
friend auto operator <= ( Decomposer &&, T value ) -> ExprLhs<T> {
354+
template <typename T,
355+
std::enable_if_t<capture_by_value<T>::value, int> = 0>
356+
constexpr friend auto operator <= ( Decomposer &&, T value ) -> ExprLhs<T> {
325357
return ExprLhs<T>{ value };
326358
}
327359
};

tests/SelfTest/UsageTests/Compilation.tests.cpp

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,12 @@ TEST_CASE("ADL universal operators don't hijack expression deconstruction", "[co
313313
REQUIRE(0 ^ adl::always_true{});
314314
}
315315
316-
TEST_CASE( "#2555 - types that can only be compared with 0 literal (not int/long) are supported", "[compilation][approvals]" ) {
316+
TEST_CASE( "#2555 - types that can only be compared with 0 literal implemented as pointer conversion are supported",
317+
"[compilation][approvals]" ) {
317318
REQUIRE( TypeWithLit0Comparisons{} < 0 );
318319
REQUIRE_FALSE( 0 < TypeWithLit0Comparisons{} );
319320
REQUIRE( TypeWithLit0Comparisons{} <= 0 );
320-
REQUIRE_FALSE( 0 > TypeWithLit0Comparisons{} );
321+
REQUIRE_FALSE( 0 <= TypeWithLit0Comparisons{} );
321322
322323
REQUIRE( TypeWithLit0Comparisons{} > 0 );
323324
REQUIRE_FALSE( 0 > TypeWithLit0Comparisons{} );
@@ -330,6 +331,72 @@ TEST_CASE( "#2555 - types that can only be compared with 0 literal (not int/long
330331
REQUIRE_FALSE( 0 != TypeWithLit0Comparisons{} );
331332
}
332333
334+
// These tests require `consteval` to propagate through `constexpr` calls
335+
// which is a late DR against C++20.
336+
#if defined( CATCH_CPP20_OR_GREATER ) && defined( __cpp_consteval ) && \
337+
__cpp_consteval >= 202211L
338+
// Can't have internal linkage to avoid warnings
339+
void ZeroLiteralErrorFunc();
340+
namespace {
341+
struct ZeroLiteralConsteval {
342+
template <class T, std::enable_if_t<std::is_same_v<T, int>, int> = 0>
343+
consteval ZeroLiteralConsteval( T zero ) noexcept {
344+
if ( zero != 0 ) { ZeroLiteralErrorFunc(); }
345+
}
346+
};
347+
348+
// Should only be constructible from literal 0. Uses the propagating
349+
// consteval constructor trick (currently used by MSVC, might be used
350+
// by libc++ in the future as well).
351+
struct TypeWithConstevalLit0Comparison {
352+
# define DEFINE_COMP_OP( op ) \
353+
constexpr friend bool operator op( TypeWithConstevalLit0Comparison, \
354+
ZeroLiteralConsteval ) { \
355+
return true; \
356+
} \
357+
constexpr friend bool operator op( ZeroLiteralConsteval, \
358+
TypeWithConstevalLit0Comparison ) { \
359+
return false; \
360+
}
361+
362+
DEFINE_COMP_OP( < )
363+
DEFINE_COMP_OP( <= )
364+
DEFINE_COMP_OP( > )
365+
DEFINE_COMP_OP( >= )
366+
DEFINE_COMP_OP( == )
367+
DEFINE_COMP_OP( != )
368+
369+
#undef DEFINE_COMP_OP
370+
};
371+
372+
} // namespace
373+
374+
namespace Catch {
375+
template <>
376+
struct capture_by_value<TypeWithConstevalLit0Comparison> : std::true_type {};
377+
}
378+
379+
TEST_CASE( "#2555 - types that can only be compared with 0 literal implemented as consteval check are supported",
380+
"[compilation][approvals]" ) {
381+
REQUIRE( TypeWithConstevalLit0Comparison{} < 0 );
382+
REQUIRE_FALSE( 0 < TypeWithConstevalLit0Comparison{} );
383+
REQUIRE( TypeWithConstevalLit0Comparison{} <= 0 );
384+
REQUIRE_FALSE( 0 <= TypeWithConstevalLit0Comparison{} );
385+
386+
REQUIRE( TypeWithConstevalLit0Comparison{} > 0 );
387+
REQUIRE_FALSE( 0 > TypeWithConstevalLit0Comparison{} );
388+
REQUIRE( TypeWithConstevalLit0Comparison{} >= 0 );
389+
REQUIRE_FALSE( 0 >= TypeWithConstevalLit0Comparison{} );
390+
391+
REQUIRE( TypeWithConstevalLit0Comparison{} == 0 );
392+
REQUIRE_FALSE( 0 == TypeWithConstevalLit0Comparison{} );
393+
REQUIRE( TypeWithConstevalLit0Comparison{} != 0 );
394+
REQUIRE_FALSE( 0 != TypeWithConstevalLit0Comparison{} );
395+
}
396+
397+
#endif // C++20 consteval
398+
399+
333400
namespace {
334401
struct MultipleImplicitConstructors {
335402
MultipleImplicitConstructors( double ) {}

tests/SelfTest/helpers/type_with_lit_0_comparisons.hpp

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,28 @@
1212
#include <type_traits>
1313

1414
// Should only be constructible from literal 0.
15+
// Based on the constructor from pointer trick, used by libstdc++ and libc++
16+
// (formerly also MSVC, but they've moved to consteval int constructor).
1517
// Used by `TypeWithLit0Comparisons` for testing comparison
1618
// ops that only work with literal zero, the way std::*orderings do
17-
struct ZeroLiteralDetector {
18-
constexpr ZeroLiteralDetector( ZeroLiteralDetector* ) noexcept {}
19+
struct ZeroLiteralAsPointer {
20+
constexpr ZeroLiteralAsPointer( ZeroLiteralAsPointer* ) noexcept {}
1921

2022
template <typename T,
2123
typename = std::enable_if_t<!std::is_same<T, int>::value>>
22-
constexpr ZeroLiteralDetector( T ) = delete;
24+
constexpr ZeroLiteralAsPointer( T ) = delete;
2325
};
2426

27+
2528
struct TypeWithLit0Comparisons {
26-
#define DEFINE_COMP_OP( op ) \
27-
friend bool operator op( TypeWithLit0Comparisons, ZeroLiteralDetector ) { \
28-
return true; \
29-
} \
30-
friend bool operator op( ZeroLiteralDetector, TypeWithLit0Comparisons ) { \
31-
return false; \
29+
#define DEFINE_COMP_OP( op ) \
30+
constexpr friend bool operator op( TypeWithLit0Comparisons, \
31+
ZeroLiteralAsPointer ) { \
32+
return true; \
33+
} \
34+
constexpr friend bool operator op( ZeroLiteralAsPointer, \
35+
TypeWithLit0Comparisons ) { \
36+
return false; \
3237
}
3338

3439
DEFINE_COMP_OP( < )

0 commit comments

Comments
 (0)