I ran into this while debugging this question.
I trimmed it down all the way to just using Boost Operators:
Compiler Explorer C++17 C++20
#include <boost/operators.hpp>
struct F : boost::totally_ordered1<F, boost::totally_ordered2<F, int>> {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
bool operator< (F const& o) const { return t < o.t; }
private: int t;
};
int main() {
#pragma GCC diagnostic ignored "-Wunused"
F { 42 } == F{ 42 }; // OKAY
42 == F{42}; // C++17 OK, C++20 infinite recursion
F { 42 } == 42; // C++17 OK, C++20 infinite recursion
}
This program compiles and runs fine with C++17 (ubsan/asan enabled) in both GCC and Clang.
When you change the implicit constructor to explicit
, the problematic lines obviously no longer compile on C++17
Surprisingly both versions compile on C++20 (v1 and v2), but they lead to infinite recursion (crash or tight loop, depending on optimization level) on the two lines that wouldn't compile on C++17.
Obviously this kind of silent bug creeping in by upgrading to C++20 is worrisome.
Questions:
The relational and equality operators test the following relationships: The first four operators in the list above have a higher precedence than the equality operators ( == and != ). See the precedence information in the table Precedence and Associativity of C Operators. The operands can have integral, floating, or pointer type.
The first four operators in the list above have a higher precedence than the equality operators ( == and != ). See the precedence information in the table Precedence and Associativity of C Operators. The operands can have integral, floating, or pointer type. The types of the operands can be different.
I suspect it might be due to c++20's new "spaceship operator" support, but don't understand how it changes the behaviour of this code. Show activity on this post. Indeed, C++20 unfortunately makes this code infinitely recursive. Let's just look at 42 == F {42}. In C++17, we only had one candidate: the non-member candidate ( #2 ), so we select that.
The examples below illustrate relational and equality operators. Because x and y are equal, the expression in this example yields the value 0. The fragment in this example sets each element of array to a null character constant. enum color { red, white, green } col; . . . if ( col == red ) . . .
Indeed, C++20 unfortunately makes this code infinitely recursive.
Here's a reduced example:
struct F {
/*implicit*/ F(int t_) : t(t_) {}
// member: #1
bool operator==(F const& o) const { return t == o.t; }
// non-member: #2
friend bool operator==(const int& y, const F& x) { return x == y; }
private:
int t;
};
Let's just look at 42 == F{42}
.
In C++17, we only had one candidate: the non-member candidate (#2
), so we select that. Its body, x == y
, itself only has one candidate: the member candidate (#1
) which involves implicitly converting y
into an F
. And then that member candidate compares the two integer members and this is totally fine.
In C++20, the initial expression 42 == F{42}
now has two candidates: both the non-member candidate (#2
) as before and now also the reversed member candidate (#1
reversed). #2
is the better match - we exactly match both arguments instead of invoking a conversion, so it's selected.
Now, however, x == y
now has two candidates: the member candidate again (#1
), but also the reversed non-member candidate (#2
reversed). #2
is the better match again for the same reason that it was a better match before: no conversions necessary. So we evaluate y == x
instead. Infinite recursion.
Non-reversed candidates are preferred to reversed candidates, but only as a tiebreaker. Better conversion sequence is always first.
Okay great, how can we fix it? The simplest option is removing the non-member candidate entirely:
struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
private:
int t;
};
42 == F{42}
here evaluates as F{42}.operator==(42)
, which works fine.
If we want to keep the non-member candidate, we can add its reversed candidate explicitly:
struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
bool operator==(int i) const { return t == i; }
friend bool operator==(const int& y, const F& x) { return x == y; }
private:
int t;
};
This makes 42 == F{42}
still choose the non-member candidate, but now x == y
in the body there will prefer the member candidate, which then does the normal equality.
This last version can also remove the non-member candidate. The following also works without recursion for all test cases (and is how I would write comparisons in C++20 going forward):
struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
bool operator==(int i) const { return t == i; }
private:
int t;
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With