Ambiguous Overloaded Operator C++20
Asked Answered
B

1

5

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?

Bartlett answered 28/1, 2021 at 23:53 Comment(2)
@Barry I'm not sure if this is related to the spaceship-operator... If I replace my overloads with the default spaceship I get the same compiler errors.Bartlett
It's related to the rules that were adopted as part of the spaceship feature.Error
E
8

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.

Error answered 29/1, 2021 at 0:5 Comment(4)
I'm confused why A::operator==(A const&) is considered a valid candidate. From what I can tell there is no valid path to get an A from a B.Bartlett
@wKavey Derived-to-base? Access control is checked after overload resolution, not before.Error
Thanks for the help. I agree it is a unique situation. In hindsight the constructor should have been explicit (or the inheritance should have never existed). Now that we're here we would very much prefer to not break the interface for existing users of the library, while supporting others in C++20.Bartlett
Also derived-to-base B->A is better than user-defined A->B (but these are different arguments, so it's still a crisscross situation).Adscititious

© 2022 - 2024 — McMap. All rights reserved.