Reputation: 6480
Consider the following code, a simple Struct with all of the pre-C++20 comparison operators defined, as well as a conversion operator to const char *
(that for this example throws, for simplicity of tracing).
struct Struct
{
int _i;
Struct( int i ) : _i( i ) {}
bool operator==( const Struct &b ) const { return _i == b._i; }
bool operator!=( const Struct &b ) const { return _i != b._i; }
bool operator<( const Struct &b ) const { return _i < b._i; }
bool operator<=( const Struct &b ) const { return _i <= b._i; }
bool operator>( const Struct &b ) const { return _i > b._i; }
bool operator>=( const Struct &b ) const { return _i >= b._i; }
operator const char *() const { throw "Crash"; return nullptr; }
};
Now let us put that structure into a std::tuple
, just a single element for simplicity's sake. Create two of those and sort them (lexicographically, per std::tuple
):
#include <cstdio>
#include <tuple>
int main()
{
std::tuple<Struct> a( 1 ), b( 2 );
printf( "%s\n", a < b ? "Right" : "Wrong" );
return 0;
}
What does this output? Under C++17 it will print "Right"
as you may expect. Under C++20, though, the same code will throw the exception in the const char *
conversion operator.
Why? Because we haven't defined an operator <=>
in our struct, and C++20's std::tuple<Struct>
will end up calling std::operator<=><Struct, Struct>
in order to determine whether a < b
is true. Per the C++20 standard, std::tuple
only defines the operator ==
and operator <=>
comparison operators, which the compiler then uses to perform the <
operation.
What's surprising is std::operator<=><Struct, Struct>
ends up being producing code equivalent to (const char *) <=> (const char *)
. It ignores the Struct
comparison operators that could have otherwise been used to synthesize operator <=>
, in favor of the conversion operator.
In practice this means that our std::tuple<Struct>
had a well-defined ordering in C++17 that now takes a different codepath through the operator const char *
conversion, which results in different behavior at run-time.
My question:
Other than manually looking at all instantiations of std::tuple
and verifying that either lexicographic comparisons are not performed, there are no conversion operators, or that any classes or structures contained within define operator <=>
, is there a way to identify at compile-time that this problem exists in a large codebase?
Upvotes: 4
Views: 539
Reputation: 303576
Other than manually looking at all instantiations of
std::tuple
and verifying that either lexicographic comparisons are not performed, there are no conversion operators, or that any classes or structures contained within define operator<=>
, is there a way to identify at compile-time that this problem exists in a large codebase?
There, unfortunately, definitely isn't something you can do within C++ to verify this in any way.
But at least you don't have to look at std::tuple
(or std::pair
or std::vector
or ..., which behave the same) - you need to look for types, T
, that:
<=>
directly, and<
, andU
, whereU
does provide a <=>
, andU
's <=>
is a builtin.This basically requires U
to be a builtin type, because when we're evaluating t1 <=> t2
, in order for U(t1) <=> U(t2)
to be a candidate function, it needs to be either a builtin or an operator<=>
candidate that is found by ADL on T
-- but in order for it to be a candidate it would need to be a non-member, non-template candidate. operator<=>
should be a member function or, if not that, a hidden friend or, if not that, a non-member function template. None of those options would work.
This list seems like something that would be possible to write a clang-tidy check for. One difficulty would be that both <=>
and <
can be defined outside of the class. And also you have class templates to consider.
At least, the fix is easy once you run into the problem: provide a <=>
. The following works in both C++17 and C++20:
struct Struct
{
int _i;
Struct( int i ) : _i( i ) {}
bool operator==( const Struct &b ) const { return _i == b._i; }
#ifdef __cpp_lib_three_way_comparison
std::strong_ordering operator<=>( const Struct &b ) const { return _i <=> b._i; }
#else
bool operator!=( const Struct &b ) const { return _i != b._i; }
bool operator<( const Struct &b ) const { return _i < b._i; }
bool operator<=( const Struct &b ) const { return _i <= b._i; }
bool operator>( const Struct &b ) const { return _i > b._i; }
bool operator>=( const Struct &b ) const { return _i >= b._i; }
#endif
operator const char *() const { throw "Crash"; return nullptr; }
};
In this particular case, you could even default operator<=>
, but in the general case that may not be true, so I'd rather write it out for the example.
Upvotes: 2