C++20 中多重继承和宇宙飞船运算符的歧义

Ambiguity in case of multiple inheritance and spaceship operator in C++20

在下面的简化程序中,struct C 继承自两个struct AB。前者同时定义了 spaceship operator <=> 和 less 运算符,后者只定义了 spaceship 运算符。然后对classC:

的对象进行less操作
#include <compare>

struct A { 
    auto operator <=>(const A&) const = default;
    bool operator <(const A&) const = default;
};
struct B { 
    auto operator <=>(const B&) const = default; 
};
struct C : A, B {};
int main() { 
    C c;
    c.operator<(c); //ok everywhere
    return c < c;   //error in GCC
}

这里令人惊讶的时刻是显式调用 c.operator<(c) 在所有编译器中都成功,但类似的调用 c<c 被 Clang 允许但在 GCC 中被拒绝:

error: request for member 'operator<=>' is ambiguous
<source>:8:10: note: candidates are: 'auto B::operator<=>(const B&) const'
<source>:4:10: note:                 'constexpr auto A::operator<=>(const A&) const'

演示:https://gcc.godbolt.org/z/xn7W9PaPc

有一个可能相关的问题:。但是在那个问题中,显式运算符 (++) 调用被所有编译器拒绝,不像这个问题,显式运算符调用被所有人接受。

我还以为C里面只有一个operator <,是从A派生出来的,根本不用考虑星舰操作员。是这样吗?这里是什么编译器?

gcc 在这里是正确的。

当你这样做时:

c.operator<(c);

您正在对字面上名为 operator< 的内容执行名称查找。只有一个这样的函数(A 中的那个)所以成功了。

但是当您执行 c < c 时,您并不是在查找 operator<。你在做两件事:

  1. 针对 c < c 的特定查找找到 operator< 个候选人(成员、非成员或内置)
  2. 找到 c <=> c
  3. 的所有重写候选项

现在,第一次查找成功并找到与之前相同的 A::operator<。但是第二次查找失败了——因为 c <=> c 不明确(在 AB 中的候选者之间)。 [class.member.lookup]/6 的规则是:

The result of the search is the declaration set of S(N,T). If it is an invalid set, the program is ill-formed.

我们有一个无效的搜索结果集,因此程序格式错误。不是我们什么都没找到,而是整个查找失败了。仅仅因为在这种情况下我们正在查找重写的候选而不是主要候选并不重要,它仍然是一个失败的查找。


失败实际上是件好事,因为如果我们以通常的方式解决这个不明确的合并集问题:

  struct C : A, B {
+     using A::operator<=>;
+     using B::operator<=>;
  };

那么我们的查询就会有歧义!因为现在我们对重写的候选项的查找找到了两个 operator<=>,所以我们最终得到三个候选项:

  1. operator<(A const&, A const&)
  2. operator<=>(A const&, A const&)
  3. operator<=>(B const&, B const&)

1 优于 2(因为主要候选人优于重写的候选人),但 13 是模棱两可的(两者都不优于另一个)。

因此,原版失败了,这个也失败了,这是件好事:作为 class 作者,由你来想出正确的事情来做——因为那是什么并不明显是。

我向 Microsoft 报告了这个问题,他们告诉我他们和 Clang 的行为是正确的,而 GCC 是错误的:https://developercommunity.visualstudio.com/t/False-acceptance-of-ambiguity-in-case-of/1534112

为了完整起见,让我在这里引用他们的回答:

The compiler behavior you’re observing is by design as per the resolution outlined in https://cdacamar.github.io/wg21papers/proposed/spaceship-dr.html. The reason is that the compiler will find both A::operator<=>, A::operator<, and B::operator<=> as possible overload resolution candidates. Because A::operator< does not require rewriting the expression the compiler will not consider A::operator<=> because A::operator< is declared in the same scope with the same signature so the only remaining candidates are A::operator< and B::operator<=> but since <=> requires rewriting the expression it is dropped later and A::operator< is selected. You can observe that Clang has the same behavior here: https://godbolt.org/z/M6ffr95Ej