Ambiguity in case of multiple inheritance and spaceship operator in C++20

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?

Answer

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:

  1. a specific lookup for c < c which finds operator< candidates (member, non-member, or builtin)
  2. finding all rewritten candidates for 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:

  1. operator<(A const&, A const&)
  2. operator<=>(A const&, A const&)
  3. 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.