Home > Net >  Different results for overloaded templated equality comparison operator with C 20 between gcc and M
Different results for overloaded templated equality comparison operator with C 20 between gcc and M

Time:11-06

Consider the following implementation of equality operators, compiled with C 20 (live on godbolt):

#include <optional>

template <class T>
struct MyOptional{
    bool has_value() const { return false;}
    T const & operator*() const { return t; }
    T t{};
};

template <class T>
bool operator==(MyOptional<T> const & lhs, std::nullopt_t)
{
    return !lhs.has_value();
}

template <class U, class T>
bool operator==(U const & lhs, MyOptional<T> const & rhs)
{
    // gcc error: no match for 'operator==' (operand types are 'const std::nullopt_t' and 'const int')
    return rhs.has_value() ? lhs == *rhs : false;
}

int main(){
    MyOptional<int> o1;
    bool compiles = o1 == std::nullopt;
    bool doesNotCompile = std::nullopt == o1; // gcc fails
}

Both clang 15 and MSVC 19.33 compile this without errors, but gcc 12.2 gives

<source>: In instantiation of 'bool operator==(const U&, const MyOptional<T>&) [with U = std::nullopt_t; T = int]':
<source>:26:43:   required from here
<source>:20:34: error: no match for 'operator==' (operand types are 'const std::nullopt_t' and 'const int')
   20 |     return rhs.has_value() ? lhs == *rhs : false;
      |                              ~~~~^~~~~~~
<source>:11:6: note: candidate: 'template<class T> bool operator==(const MyOptional<T>&, std::nullopt_t)' (reversed)
   11 | bool operator==(MyOptional<T> const & lhs, std::nullopt_t)
      |      ^~~~~~~~
<source>:11:6: note:   template argument deduction/substitution failed:
<source>:20:34: note:   mismatched types 'const MyOptional<T>' and 'const int'
   20 |     return rhs.has_value() ? lhs == *rhs : false;
      |                              ~~~~^~~~~~~
<source>:17:6: note: candidate: 'template<class U, class T> bool operator==(const U&, const MyOptional<T>&)' (reversed)
   17 | bool operator==(U const & lhs, MyOptional<T> const & rhs)
      |      ^~~~~~~~
<source>:17:6: note:   template argument deduction/substitution failed:
<source>:20:34: note:   'const std::nullopt_t' is not derived from 'const MyOptional<T>'
   20 |     return rhs.has_value() ? lhs == *rhs : false;
      |  

gcc apparently thinks that the second overload is a better match for std::nullopt == o1 and attempts to call it.

To me, it is not clear which compiler is right. C 20 introduced the automatic reversal of the arguments when comparing for equality. As far as I understand it, when the compiler sees std::nullopt == o1, it also considers o1 == std::nullopt. However, I failed to find a statement anywhere when the reversed order of arguments is considered:

  1. Should the compiler first search for valid calls for std::nullopt == o1, and if it found any, use it directly and never search for o1 == std::nullopt? In this case, I guess, gcc would be right?
  2. Or, should the compiler always consider both std::nullopt == o1 and the reversed arguments o1 == std::nullopt, and from the set of viable functions select the "best" match (whatever "best" means here if the original and reversed arguments match different overloads)? I guess, in this case MSVC and clang are right?

So, the question is: Which compiler is right, and when in the matching process is the reversed order of arguments considered?

CodePudding user response:

The rewritten candidates are considered at the same time as the non-rewritten ones. There is only a late tie breaker in the overload resolution rules if neither candidate is better by the higher priority rules. (See [over.match.best.general]/2 for the full decision chain.)

A candidate is considered better than another if at least one conversion sequence of an argument to a parameter is considered better than for the other candidate and none are considered worse. In your case all conversion sequences are equally good.

So the tie breakers need to be considered. The next relevant tie breaker before the one considering whether the candidate is a rewritten one, is partial ordering of templates from which the candidates were formed (applying here since both candidates come from templates).

Partial ordering of templates considers whether or not one template is more specialized than the other, which basically asks whether the set of argument lists accepted by one template is a strict subset of that of the other one. (The actual rules are quite complicated.)

So the question here is which templates exactly should be compared for this? If we just compare the two templates as you have written them, then neither is more specialized. But if we consider that one candidate was rewritten from the template, then probably this test should also consider the parameters of the template to be reversed? With that being the case, the first template would be more specialized, because it only accepts one type as its first argument, while the other accepts any type. And so the first template should be chosen.

If we do not consider this reversing, then we would fall through to the tie breaker considering rewriting and because the first template had to be rewritten to be a candidate for the operator, the second one would be chosen.

Clang and MSVC are following the reversing for partial ordering, while GCC is not.

This is CWG 2445 which was resolved to state that this reversing should be done, so that Clang and MSVC are correct. GCC seems to not have implemented the defect report yet.

  • Related