In the following simplified program, struct C
inherits from two structs A
and B
. The former defines both spaceship operator <=>
and less operator, the latter – only spaceship operator. Then less operation is performed with objects of class C
:
#include <compare>
struct A {
auto operator <=>(const A&) const = default;
bool operator <(const A&) const = default;
};
struct B {
auto operator <=>(const B&) const = default;
};
struct C : A, B {};
int main() {
C c;
c.operator<(c); //ok everywhere
return c < c; //error in GCC
}
The surprising moment here is that the explicit call c.operator<(c)
succeeds in all compliers, but the similar call c<c
is permitted by Clang but rejected in GCC:
error: request for member 'operator<=>' is ambiguous
<source>:8:10: note: candidates are: 'auto B::operator<=>(const B&) const'
<source>:4:10: note: 'constexpr auto A::operator<=>(const A&) const'
Demo: https://gcc.godbolt.org/z/xn7W9PaPc
There is a possibly related question: GCC can't differentiate between operator++() and operator++(int) . But in that question the explicit operator (++) call is rejected by all compilers, unlike this question where explicit operator call is accepted by all.
I thought that only one operator <
is present in C
, which was derived from A
, and starship operator shall not be considered at all. Is it so and what compiler is right here?
gcc is correct here.
When you do:
c.operator<(c);
You are performing name lookup on something literally named operator<
. There is only one such function (the one in A
) so this succeeds.
But when you do c < c
, you're not doing lookup for operator<
. You're doing two things:
c < c
which finds operator<
candidates (member, non-member, or builtin)c <=> c
Now, the first lookup succeeds and finds the same A::operator<
as before. But the second lookup fails - because c <=> c
is ambiguous (between the candidates in A
and B
). And the rule, from [class.member.lookup]/6 is:
The result of the search is the declaration set of
S(N,T)
. If it is an invalid set, the program is ill-formed.
We have an invalid set as the result of the search, so the program is ill-formed. It's not that we find nothing, it's that the whole lookup fails. Just because in this context we're looking up a rewritten candidate rather than a primary candidate doesn't matter, it's still a failed lookup.
And it's actually good that it fails because if we fix this ambiguous merge set issue in the usual way:
struct C : A, B {
+ using A::operator<=>;
+ using B::operator<=>;
};
Then our lookup would be ambiguous! Because now our lookup for the rewritten candidates finds two operator<=>
s, so we end up with three candidates:
operator<(A const&, A const&)
operator<=>(A const&, A const&)
operator<=>(B const&, B const&)
1
is better than 2
(because a primary candidate is better than a rewritten candidate), but 1
vs 3
is ambiguous (neither is better than the other).
So the fact that the original fails, and this one also fails, is good: it's up to you as the class author to come up with the right thing to do - since it's not obvious what that is.