Search code examples
c++language-lawyeroverloadingmultiple-inheritancec++20

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?


Solution

  • 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.