为什么我必须提供“operator ==”,而“operator <=>”已经足够了?

57
#include <compare>

struct A
{
    int n;

    auto operator<=>(A const& other) const
    {
        if (n < other.n)
        {
            return std::strong_ordering::less;
        }
        else if (n > other.n)
        {
            return std::strong_ordering::greater;
        }
        else
        {
            return std::strong_ordering::equal;
        }
    }

    // compile error if the following code is commented out.
    // bool operator==(A const& other) const
    // { return n == other.n; }
};

int main()
{   
    A{} == A{};
}

请查看在线演示

为什么必须提供operator ==operator <=>已经足够?


5
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1190r0.html - jamesdlin
6
你知道的,我提供的第二个重复问题链接也是由你提出的…… - StoryTeller - Unslander Monica
3
这个问题是另一个问题的重复:https://dev59.com/9lMH5IYBdhLWcg3wqg5V ,但我不想将其关闭为重复,因为这里的答案提供了不同的信息/不同的观点来帮助理解相同的信息... - Justin
1
@Justin - 我认为这是一个重复的问题。不是你提供的那个,而是12。不幸的是,我已经投票了,只是为了让这个问题在可疑的情况下重新打开。 - StoryTeller - Unslander Monica
1
@Justin 管理员可以合并答案,这个问题应该关闭,以便所有答案可以共存。 - OrangeDog
显示剩余15条评论
5个回答

64
为什么必须提供 operator==,当 operator<=> 足够了呢?
嗯,主要是因为它 不够 :-)
当 C++ 重写您的语句时,相等和排序是不同的概念: | | 相等 | 排序 | |--------|------|------| | 主要 | == | <=> | | 次要 | != | <, >, <=, >= |
主要运算符具有反转的能力,而次要运算符具有在其对应的主要运算符的条件下被重写的能力:
- 反转意味着 a == b 可以是: - 如果可用:a.operator==(b) - 如果不可用:b.operator==(a) - 重写意味着 a != b 可以是: - 如果可用:! a.operator==(b)
最后一个可能也可以是 ! b.operator==(a) 如果您必须重写并反转它(我不完全确定,因为我的经验大多数是比较相同类型)。
但是,重写不能跨越相等/排序边界,这意味着 <=> 不是 == 的重写候选项。
原因在于分离相等和排序可以在此 P1185 论文中找到,该论文是讨论此问题的众多标准会议之一。
有许多情况下,自动使用 <=> 实现 == 可能会非常低效。例如字符串、向量、数组或任何其他集合。您可能不想使用 <=> 来检查两个字符串的相等性。
这是因为<=>需要处理整个字符串才能确定顺序,然后检查顺序是否相等。但是,一个简单的长度检查可以很快地告诉你它们不相等。这就是O(n)时间复杂度、十亿级别的比较和O(1)几乎立即得出结果之间的区别。
如果你知道它没问题(或者你愿意接受任何可能带来的性能损失),你总是可以默认相等。但最好不要让编译器替你做出这个决定。
更详细地说,考虑以下完整程序:
#include <iostream>
#include <compare>

class xyzzy {
public:
    xyzzy(int data) : n(data) { }

    auto operator<=>(xyzzy const &other) const {
        // Could probably just use: 'return n <=> other.n;'
        // but this is from the OPs actual code, so I didn't
        // want to change it too much (formatting only).

        if (n < other.n) return std::strong_ordering::less;
        if (n > other.n) return std::strong_ordering::greater;
        return std::strong_ordering::equal;
    }

    //auto operator==(xyzzy const &other) const {
    //    return n == other.n;
    //}

    //bool operator==(xyzzy const &) const = default;

private:
    int n;
};

int main() {
    xyzzy twisty(3);
    xyzzy passages(3);

    if (twisty < passages) std::cout << "less\n";
    if (twisty == passages) std::cout << "equal\n";
}

它无法直接编译,因为最后一个语句需要一个“operator==”。但是您不必提供一个“真正的”(第一个被注释掉的块),您可以告诉它使用默认值(第二个)。在这种情况下,这可能是正确的决定,因为使用默认值没有实际性能影响。
请记住,只有在您明确提供三向比较运算符(并且当然使用“==”或“!=”)时才需要提供相等运算符。如果两者都不提供,则C ++将为您提供两个默认值。
即使您必须提供两个函数(其中一个可能是默认函数),这仍然比以前好,以前您必须明确提供它们全部,例如:
- a == b。 - a < b。 - a != b,定义为!(a == b)。 - a> b,定义为!(a = b,定义为!(a

16
感谢编辑我的答案并使用新的表格格式,Aryan。我甚至不知道那是一个选项(你会认为已经在这里十年的人会知道类似的事情)。有了这个新的知识,我的未来答案将会大有改善,至少在格式方面,如果不是内容方面的话 :-)。 - paxdiablo
5
为了公平起见,表格格式仅限于7个月以下(参见链接:https://meta.stackexchange.com/questions/356997/new-feature-table-support)。 - justhalf

11
为什么我必须提供“operator ==”,而“operator <=>”就足够了?
因为它不会被使用。
如果你使用默认的操作符,那就足够了。
struct A
{
    int n;
    auto operator<=>(A const& other) const = default;
};

基本上,n == n 的效率可能比 (a <=> a) == std::strong_ordering::equal 更高,有许多情况可以选择使用前者。当你提供自定义的 <=> 时,语言实现无法知道后者是否可以用前者替换,也无法知道后者是否不必要地低效。

因此,如果你需要自定义三路比较,则需要自定义相应的等式比较。这个示例类不需要自定义三路比较,所以应该使用默认的。


1
这只会导致警告,而不是错误吧? - einpoklum
那么我不明白为什么要强制用户编写额外的代码,只是因为否则效率会降低。例如,C++并不强制用户编写移动构造函数,这经常导致大量无用的复制。 - einpoklum
编译器会在不需要编写的情况下隐式生成移动构造函数。 - eerorika
一般情况下,不会。在某些特定条件下,可能会发生。 - einpoklum
4
“土豆,土豆”这种说法写出来就没有效果了,看起来就像作者疯了一样。也许“potayto,potarto”会更好。 - paxdiablo
显示剩余3条评论

7

因为有时候使用==比使用a <=> b == 0实现更快,所以编译器默认不会使用潜在的次优实现。

比如说,考虑std::string,它可以在循环遍历元素之前检查大小是否相同。

请注意,您不必手动实现==。 您可以将其=default,这将使用<=>来实现它。

还要注意,如果您=default<=>本身,则不需要=default ==


1
和@errorika一样的问题:是的,慢速代码应该可以编译。我认为警告就足够了。 - einpoklum
@einpoklum 标准实际上并没有警告的概念。它不能强制执行警告。 - HolyBlackCat
虽然如此 - 我在您的回答中看不到==被“必须”的理由,而非仅仅是推荐。 - einpoklum
@einpoklum 你有什么问题吗?在这种情况下,委员会决定拒绝潜在的缓慢代码,所以现在我们就是这样。 - HolyBlackCat
问题是为什么?C++很少甚至从不拒绝其他有效的代码,因为它可能不够快。 - einpoklum
我相信唯一的原因是性能。如果你认为还有其他原因,可以试着找出提案... - HolyBlackCat

7

看前面的回答,没有人提到另一个问题:为了排序,两个对象可能是等效的,但却不相等。例如,我可能希望按Unicode NFC规范化和折叠大小写来对字符串进行排序,但对于相等性测试,我想验证字符串实际上是相同的,大小写很重要,甚至在输入中区分é和´+e。

是的,这是一个有点虚构的例子,但它可以突显一个观点,即<=>的定义并不要求排序,因此您不能依赖于<=>甚至可能返回std::strong_ordering::equal。将==默认为<=>返回std::strong_ordering::equal不能假定为有效的实现。


6
如果一个类型这样做,那么我会认为它是有缺陷的。对我来说,这将是一个合理的默认实现(如果没有性能问题),因为你总是可以覆盖它。 - HolyBlackCat
2
是的,我不确定我能够信任那些在排序和相等性方面使用不同规则的类型。这可能会导致奇怪的情况,比如对它们进行排序会使得“相等”的元素完全分离 :-) - paxdiablo
4
我猜你不太信任IEEE-754格式的浮点数 :) - alephzero
@alephzero Rust 也不信任它们。 - wizzwizz4
1
不错的观察。Unicode排序算法(#UTS10)花了一些力气来区分排序和比较,并解释了为什么这很重要,包括在A.3确定性比较11.1排序折叠等章节中。 - tchrist

3
不,你不需要。 只需添加。
  bool operator==(A const& other) const = default;

https://godbolt.org/z/v1WGhxca6

您可以随时重载它们以不同的语义。为了防止意外生成的函数,需要使用 = default


这不会使用 operator<=> - maowtm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接