Search code examples
c++operator-overloadingc++20ambiguousspaceship-operator

Ambiguous Overloaded Operator C++20


I'm trying to test my project in the latest Visual Studio and Clang versions. One of the errors that pops up is related to an ambiguous operator (with reversed parameter order). This does not seem to pop up in C++17.

For example: (https://godbolt.org/z/Gazbbo)

struct A {
    bool operator==(const A& other) const { return false; }
};

struct B : private A {
    B(const A&);
    bool operator==(const B& other) const { return false; }
};

bool check(A a, B b) {
    return b == a;
}

I'm unsure why this would be an issue. It seems to me that the only viable function here is bool operator==(const B& other) const as A can be implicitly converted to B but not the reverse. Indeed, if I mark B(const A&) with explicit I instead get an error that B can not be converted to the private base of A.

I'm trying to understand what I can do to avoid this, aside from using explicit or using B(a). Imagine A and B were library code, how do I support C++20 without breaking my interface in lower versions?


Solution

  • In C++17, yes, the only viable candidate was b.operator==(B(a)).


    But in C++20, comparison operators have more functionality. Equality can now consider reversed and rewritten candidates as well. So when consider the expression b == a we also consider the expression a == b. As a result, we have two candidates:

    bool B::operator==(B const&);
    bool A::operator==(A const&); // reversed
    

    The B member function is an exact match on the left-hand side but requires converting the 2nd argument. The A member function is an exact match on the right-hand side but requires converting the 1st argument. Neither candidate is better than the other, so the result becomes ambiguous.

    As for how to fix it. This is kind of a strange scenario (B both inherits from A and is constructible from A?). if you drop the inheritance, you remove the A member candidate. If you remove the B(A const&) constructor, then you get an access violation since the only candidate is the one comparing A's which requires converting b to its A (which kind of suggests that this is questionable).

    Alternatively, you could add, to B, a direct comparison to A to define what that actually means. Since the issue here is that there are two choices, the compiler doesn't know which one is best, so just provide a better one:

    struct B  : private A {
        B(const A&);
        bool operator==(B const&) const;
        bool operator==(A const&) const; // <== add this one
    };
    

    Now this new one is an exact match in both arguments and is the strictly superior candidate.