Overloading operator== with `&&` and `const` qualifier cause ambiguity in C++20
Asked Answered
D

1

12

Consider the struct S with two operator== overloads of same && qualifier and different const qualifier:

struct S {
  bool operator==(const S&) && { 
    return true;
  }
  bool operator==(const S&) const && { 
    return true;
  }
};

If I compare the two S with operator==:

S{} == S{};

gcc and msvc accept this code, clang rejects it with:

<source>:14:7: error: use of overloaded operator '==' is ambiguous (with operand types 'S' and 'S')
  S{} == S{};
  ~~~ ^  ~~~

Why does clang think there is an ambiguous overload resolution here? Shouldn't the non-const one be the best candidate in this case?

Similarly, if I compare two S with the synthesized operator!=:

S{} != S{};

gcc still accept this code, but msvc and clang doesn't:

<source>:14:7: error: use of overloaded operator '!=' is ambiguous (with operand types 'S' and 'S')
  S{} != S{};
  ~~~ ^  ~~~

It seems weird that the synthesized operator!= suddenly causes the ambiguity for msvc. Which compiler is right?

Distiller answered 23/3, 2021 at 10:26 Comment(2)
Candidates for (S&&, S&&) are (S&&, const S&&), (const S&&, const S&&), (const S&&, S&&)/*rewritten*/. each S&& is better match than const S&&, so call should be ambiguous IMO.Organic
I also think Clang is correct, as @Organic has pointed out. S{} is not const, but the expression S{} == S{} will need to choose between the best match, argument per argument, say arg1 and arg2, among four synthesized overloads, and for arg1 (S&&, const S&&) will be ambigious to (S&&, S&&) (and conversely for arg2). If we compare to when using function call notation (which Clang accepts), S{}.operator(S{}), the overload set will be (const S&&, const S&) and (S&&, const S&), for which the latter is unambigiously a best match.Sarnoff
S
8

The example would be unambiguous in C++17. C++20 brings change:

[over.match.oper]

For a unary operator @ with an operand of type cv1 T1, and for a binary operator @ with a left operand of type cv1 T1 and a right operand of type cv2 T2, four sets of candidate functions, designated member candidates, non-member candidates, built-in candidates, and rewritten candidates, are constructed as follows:

  • ...
  • For the operator ,, the unary operator &, or the operator ->, the built-in candidates set is empty. For all other operators, the built-in candidates include all of the candidate operator functions defined in [over.built] that, compared to the given operator,
    • have the same operator name, and
    • accept the same number of operands, and
    • accept operand types to which the given operand or operands can be converted according to [over.best.ics], and
    • do not have the same parameter-type-list as any non-member candidate that is not a function template specialization.

The rewritten candidate set is determined as follows:

  • ...
  • For the equality operators, the rewritten candidates also include a synthesized candidate, with the order of the two parameters reversed, for each non-rewritten candidate for the expression y == x.

Thus, the rewritten candidate set includes these:

 implicit object parameter
 |||
(S&&, const S&);       // 1
(const S&&, const S&); // 2

// candidates that match with reversed arguments
(const S&, S&&);       // 1 reversed
(const S&, const S&&); // 2 reversed

The overload 1 is better match than 2, but the synthesised reversed overload of 1 is ambiguous with the original non-reversed overload because both have const conversion to one parameter. Note that this is actually ambiguous even if overload 2 doesn't exist.

Thus, Clang is correct.


This is also covered by the informative compatibility annex:

Affected subclause: [over.match.oper] Change: Equality and inequality expressions can now find reversed and rewritten candidates.

Rationale: Improve consistency of equality with three-way comparison and make it easier to write the full complement of equality operations.

Effect on original feature: Equality and inequality expressions between two objects of different types, where one is convertible to the other, could invoke a different operator. Equality and inequality expressions between two objects of the same type could become ambiguous.

struct A {
  operator int() const;
};

bool operator==(A, int);        // #1
// #2 is built-in candidate: bool operator==(int, int);
// #3 is built-in candidate: bool operator!=(int, int);

int check(A x, A y) {
  return (x == y) +             // ill-formed; previously well-formed
    (10 == x) +                 // calls #1, previously selected #2
    (10 != x);                  // calls #1, previously selected #3
}
Setscrew answered 23/3, 2021 at 11:14 Comment(8)
I'm a little confused. There seems to be issues with constness as well. Here is an example. Sometimes it warns about ambiguity, sometimes it errors and sometimes it's ok.Bird
@Bird Constness is the issue in the OP's example. Your A is OK because both parameters are const. There cannot be synthesised reversed overload with same parameter list, so there is no ambiguity. B has differing constness like OP's example. It can be fixed by making the parameters the same type using const qualifier as you did in F. C is unambiguous like A except there are additional candidates that are unambiguously lower rank matches. D is ambiguous like B but with extra candidates like C. E isn't like D because there is no comparison operator for itself (likely copy paste error).Setscrew
Whoops E was a copy paste error, yes. Ok got it now, thank you. One last question tho: why is B only a warning?Bird
@Bird For definitive answer, you'll need to ask that from Clang authors. But I suspect that part of the reasoning is to make backwards incompatibility "softer" by allowing previously well defined programs to continue working as a language extension.Setscrew
Oh so it should be an error if it was standard conform?Bird
@Bird No, standard doesn't require ill-formed programs to be errors. Diagnostic message is sufficient for standard conformance.Setscrew
Let me rephrase: according to the C++20 standard the use of B in line 36 is ill-formed, correct?Bird
@Bird Yes, the comparison / overload resolution is ill-formed. Clang is correct to diagnose the ill-formedness of the program. Other compilers that do not diagnose the issue do not conform to the standard.Setscrew

© 2022 - 2024 — McMap. All rights reserved.