gcc 和 MSVC/clang 之间使用 C++20 重载模板化相等比较运算符的不同结果

Gug*_*ugi 2 c++ gcc language-lawyer equality-operator c++20

考虑以下相等运算符的实现,用 C++20 编译(在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
}
Run Code Online (Sandbox Code Playgroud)

clang 15 和 MSVC 19.33 编译它没有错误,但 gcc 12.2 给出

<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;
      |  
Run Code Online (Sandbox Code Playgroud)

gcc 显然认为第二个重载更匹配std::nullopt == o1并尝试调用它。

对我来说,不清楚哪个编译器是正确的。C++20 引入了在比较相等性时自动反转参数。据我了解,当编译器看到 时std::nullopt == o1,它也会考虑o1 == std::nullopt. 但是,当考虑参数的相反顺序时,我在任何地方都找不到声明:

  1. 编译器是否应该首先搜索 的有效调用std::nullopt == o1,如果找到,则直接使用它而不搜索o1 == std::nullopt?在这种情况下,我想 gcc 是对的吗?
  2. 或者,编译器是否应该始终考虑std::nullopt == o1 反向参数o1 == std::nullopt,并从可行函数集中选择“最佳”匹配(如果原始参数和反向参数匹配不同的重载,则“最佳”在这里意味着什么)?我想,在这种情况下 MSVC 和 clang 是对的吗?

那么,问题是:哪种编译器是正确的,以及在匹配过程中何时考虑参数的相反顺序?

use*_*522 7

重写的候选者与未重写的候选者同时被考虑。如果两个候选者均不符合较高优先级规则,则在重载解决规则中只有最后的决胜局。(有关完整的决策链,请参阅[over.match.best.general]/2 。)

如果参数到参数的至少一个转换序列被认为比另一个候选者更好并且没有一个被认为更差,则认为一个候选者比另一个更好。在您的情况下,所有转换序列都同样好。

因此需要考虑平局破坏者。在考虑候选是否是重写模板之前的下一个相关决胜局是对形成候选的模板进行部分排序(此处应用,因为两个候选都来自模板)。

模板的部分排序考虑一个模板是否比另一个模板更专业,这基本上询问一个模板接受的参数列表集是否是另一个模板接受的参数列表集的严格子集。(实际规则相当复杂。)

所以这里的问题是到底应该比较哪些模板?如果我们只是比较您编写的两个模板,那么它们都不是更专业。但如果我们考虑到一个候选者是从模板重写的,那么这个测试是否也应该考虑模板的参数被反转?在这种情况下,第一个模板会更加专业,因为它只接受一种类型作为其第一个参数,而另一个接受任何类型。所以应该选择第一个模板。

如果我们不考虑这种逆转,那么我们将陷入考虑重写的决胜局,并且因为必须重写第一个模板才能成为运算符的候选者,所以将选择第二个模板。

Clang 和 MSVC 遵循部分排序的逆向,而 GCC 则不然。

这是CWG 2445,它已决定声明应该执行此反转,以便 Clang 和 MSVC 是正确的。GCC 似乎还没有实施缺陷报告。