Breaking change in C++20 or regression in clang-trunk/gcc-trunk when overloading equality comparison with non-Boolean return value?
Yes, the code in fact breaks in C++20.
The expression Foo{} != Foo{}
has three candidates in C++20 (whereas there was only one in C++17):
Meta operator!=(Foo& /*this*/, const Foo&); // #1
Meta operator==(Foo& /*this*/, const Foo&); // #2
Meta operator==(const Foo&, Foo& /*this*/); // #3 - which is #2 reversed
This comes from the new rewritten candidate rules in [over.match.oper]/3.4. All of those candidates are viable, since our Foo
arguments are not const
. In order to find the best viable candidate, we have to go through our tiebreakers.
The relevant rules for best viable function are, from [over.match.best]/2:
Given these definitions, a viable function
F1
is defined to be a better function than another viable functionF2
if for all argumentsi
,ICSi(F1)
is not a worse conversion sequence thanICSi(F2)
, and then
- [... lots of irrelevant cases for this example ...] or, if not that, then
- F2 is a rewritten candidate ([over.match.oper]) and F1 is not
- F1 and F2 are rewritten candidates, and F2 is a synthesized candidate with reversed order of parameters and F1 is not
#2
and #3
are rewritten candidates, and #3
has reversed order of parameters, while #1
is not rewritten. But in order to get to that tiebreaker, we need to first get through that initial condition: for all arguments the conversion sequences are not worse.
#1
is better than #2
because all the conversion sequences are the same (trivially, because the function parameters are the same) and #2
is a rewritten candidate while #1
is not.
But... both pairs #1
/#3
and #2
/#3
get stuck on that first condition. In both cases, the first parameter has a better conversion sequence for #1
/#2
while the second parameter has a better conversion sequence for #3
(the parameter that is const
has to undergo an extra const
qualification, so it has a worse conversion sequence). This const
flip-flop causes us to not be able to prefer either one.
As a result, the whole overload resolution is ambiguous.
As far as I understand, this only works as long as the return type is
bool
.
That's not correct. We unconditionally consider rewritten and reversed candidates. The rule we have is, from [over.match.oper]/9:
If a rewritten
operator==
candidate is selected by overload resolution for an operator@
, its return type shall be cvbool
That is, we still consider these candidates. But if the best viable candidate is an operator==
that returns, say, Meta
- the result is basically the same as if that candidate was deleted.
We did not want to be in a state where overload resolution would have to consider the return type. And in any case, the fact that the code here returns Meta
is immaterial - the problem would also exist if it returned bool
.
Thankfully, the fix here is easy:
struct Foo {
Meta operator==(const Foo&) const;
Meta operator!=(const Foo&) const;
// ^^^^^^
};
Once you make both comparison operators const
, there is no more ambiguity. All the parameters are the same, so all the conversion sequences are trivially the same. #1
would now beat #3
by not by rewritten and #2
would now beat #3
by not being reversed - which makes #1
the best viable candidate. Same outcome that we had in C++17, just a few more steps to get there.
The Eigen issue appears to reduce to the following:
using Scalar = double;
template<class Derived>
struct Base {
friend inline int operator==(const Scalar&, const Derived&) { return 1; }
int operator!=(const Scalar&) const;
};
struct X : Base<X> {};
int main() {
X{} != 0.0;
}
The two candidates for the expression are
- the rewritten candidate from
operator==(const Scalar&, const Derived&)
Base<X>::operator!=(const Scalar&) const
Per [over.match.funcs]/4, as operator!=
was not imported into the scope of X
by a using-declaration, the type of the implicit object parameter for #2 is const Base<X>&
. As a result, #1 has a better implicit conversion sequence for that argument (exact match, rather than derived-to-base conversion). Selecting #1 then renders the program ill-formed.
Possible fixes:
- Add
using Base::operator!=;
toDerived
, or - Change the
operator==
to take aconst Base&
instead of aconst Derived&
.
[over.match.best]/2 lists how valid overloads in a set are prioritized. Section 2.8 tells us that F1
is better than F2
if (among many other things):
F2
is a rewritten candidate ([over.match.oper]) andF1
is not
The example there shows an explicit operator<
being called even though operator<=>
is there.
And [over.match.oper]/3.4.3 tells us that the candidacy of operator==
in this circumstance is a rewritten candidate.
However, your operators forget one crucial thing: they should be const
functions. And making them not const
causes earlier aspects of overload resolution to come into play. Neither function is an exact match, as non-const
-to-const
conversions need to happen for different arguments. That causes the ambiguity in question.
Once you make them const
, Clang trunk compiles.
I can't speak to the rest of Eigen, as I don't know the code, it's very large, and thus can't fit in an MCVE.