7

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?

Barry
  • 286,269
  • 29
  • 621
  • 977
Fedor
  • 17,146
  • 13
  • 40
  • 131
  • Do you use a C++20 compiler? – jacob galam Sep 19 '21 at 05:33
  • 1
    @jacobgalam The starship operator is available since C++20. So, it cannot compile without... (Btw. it's used in the linked demo code on Compiler Explorer.) – Scheff's Cat Sep 19 '21 at 05:34
  • The problem is not in the spaceship operator, the problem is in multiple inheritance, When `c < c` , you compiler gets confused by which path to follow to evaluate `<=>` operator. – foragerDev Sep 19 '21 at 07:01

2 Answers2

6

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Thanks! Just to clarify, if we remove `operator <=>` from `A` then the program shall remain ill-formed since the remaining two candidates `operator<(A const&, A const&)` and `operator<=>(B const&, B const&)` are neither better than the other? But GCC accepts such program: https://gcc.godbolt.org/z/9rPrzfxcP – Fedor Sep 19 '21 at 18:35
  • @Fedor gcc implements something slightly different than the standard rules in order to catch some other breakages. – Barry Sep 19 '21 at 20:23
0

I reported the issue to Microsoft and they tell me that their and Clang behavior is right, while GCC is wrong: https://developercommunity.visualstudio.com/t/False-acceptance-of-ambiguity-in-case-of/1534112

Let me quote their answer here for completeness:

The compiler behavior you’re observing is by design as per the resolution outlined in https://cdacamar.github.io/wg21papers/proposed/spaceship-dr.html. The reason is that the compiler will find both A::operator<=>, A::operator<, and B::operator<=> as possible overload resolution candidates. Because A::operator< does not require rewriting the expression the compiler will not consider A::operator<=> because A::operator< is declared in the same scope with the same signature so the only remaining candidates are A::operator< and B::operator<=> but since <=> requires rewriting the expression it is dropped later and A::operator< is selected. You can observe that Clang has the same behavior here: https://godbolt.org/z/M6ffr95Ej

Fedor
  • 17,146
  • 13
  • 40
  • 131