== 和 != 是相互依赖的吗?

Are == and != mutually dependent?

提问人:BarbaraKwarc 提问时间:6/14/2016 最后编辑:Baum mit AugenBarbaraKwarc 更新时间:9/7/2023 访问量:24262

问:

我正在学习 C++ 中的运算符重载,我看到它只是一些可以为用户定义类型自定义的特殊函数。但是,我担心的是,为什么需要两个单独的定义?我认为如果是真的,那么它会自动为假,反之亦然,没有其他可能性,因为根据定义,是.我无法想象任何情况都不是真的。但也许我的想象力有限,或者我对某些事情一无所知?==!=a == ba != ba != b!(a == b)

我知道我可以用另一个来定义一个,但这不是我要问的。我也不是在问按价值或按身份比较对象之间的区别。或者两个对象是否可以同时相等和不相等(这绝对不是一个选项!这些东西是相互排斥的)。我要问的是:

有没有一种情况,问两个物体相等的问题确实有意义,但问它们相等就没有意义了?(从用户的角度,或从实现者的角度)

如果没有这种可能性,那么为什么 C++ 将这两个运算符定义为两个不同的函数呢?

C++ 运算符重载 相等 equality-operator

评论

15赞 Ali Caglayan 6/15/2016
两个指针可能都为 null,但不一定相等。
2赞 Dennis Jaheruddin 6/15/2016
不确定这里是否有意义,但读到这篇文章让我想到了“短路”问题。例如,可以定义 if always 为 true(或 false,或未定义),而不管是否可以计算表达式。在这种情况下,将根据定义返回正确的结果,但如果无法计算,则失败。(如果评估成本高昂,则需要花费大量时间)。'undefined' != expressiona!=b!(a==b)bb
2赞 zozo 6/16/2016
null != null 和 null == null 呢?它可以两者兼而有之......因此,如果 a != b,这并不总是意味着 a == b。
4赞 chiliNUT 6/19/2016
javascript 示例(NaN != NaN) == true
2赞 Ven 6/20/2016
eel.is/c++draft/expr.eq#2

答:

17赞 Benjamin Lindley 6/14/2016 #1

是否有任何情况可以提出关于两个的问题 对象相等确实是有道理的,但问它们不是 平等没有意义?(无论是从用户的角度来看,还是 实施者的观点)

这是一种意见。也许不是。但是语言设计者并不是无所不知的,他们决定不限制那些可能想出有意义的情境的人(至少对他们来说是这样)。

4赞 Daniel Jour 6/14/2016 #2

[..]为什么需要两个单独的定义?

需要考虑的一件事是,有可能更有效地实现其中一个运算符,而不仅仅是使用另一个运算符的否定。

(我这里的例子是垃圾,但重点仍然成立,例如,想想布隆滤镜:如果某些东西不在集合中,它们允许快速测试,但测试它是否在集合中可能需要更多的时间。

[..]根据定义,是 .a != b!(a == b)

作为程序员,你有责任做到这一点。写测试可能是一件好事。

评论

4赞 Benjamin Lindley 6/14/2016
如何不允许短路?如果 ,则不会进行评估。!((a == rhs.a) && (b == rhs.b))!(a == rhs.a)(b == rhs.b)
0赞 Oliver Charlesworth 6/14/2016
不过,这是一个不好的例子。短路在这里没有增加任何神奇的优势。
0赞 BarbaraKwarc 6/14/2016
@Oliver Charlesworth Alone 中,它不会,但是当与单独的运算符连接时,它会: 在 的情况下,一旦第一个对应元素不相等,它就会停止比较。但是在 的情况下,如果它以 实现,则需要首先比较所有相应的元素(当它们都相等时),以便能够判断它们不是不相等的:P但是,当如上例所示实现时,一旦找到第一个不相等的对,它就会停止比较。确实是一个很好的例子。==!===
0赞 Daniel Jour 6/14/2016
@BenjaminLindley 没错,我的例子完全是胡说八道。不幸的是,我想不出另一个自动取款机,这里为时已晚。
1赞 Oliver Charlesworth 6/14/2016
@BarbaraKwarc:在短路效率方面是等效的。!((a == b) && (c == d))(a != b) || (c != d)
12赞 Taywee 6/14/2016 #3

如果 and 运算符实际上并不意味着相等,就像 and 流运算符不意味着位移一样。如果将符号视为其他概念,则它们不必相互排斥。==!=<<>>

在相等性方面,如果您的用例需要将对象视为不可比,那么这可能很有意义,以便每次比较都应返回 false(或者如果您的运算符返回非 bool,则返回不可比的结果类型)。我想不出有什么具体情况可以保证这样做,但我可以看到它足够合理。

275赞 user743382 6/14/2016 #4

不希望语言自动重写,因为 when 返回 .有几个原因可以让你这样做。a != b!(a == b)a == bbool

您可能有表达式生成器对象,其中不执行任何比较,也不打算执行任何比较,而只是构建一些表示 .a == ba == b

你可能有惰性评估,其中没有也不打算直接执行任何比较,而是返回某种可以在以后的某个时间隐式或显式转换为实际比较的评估。可能与表达式生成器对象结合使用,以便在计算之前进行完整的表达式优化。a == blazy<bool>bool

您可能有一些自定义模板类,其中给定了可选变量和 ,您希望允许 ,但使其返回 。optional<T>tut == uoptional<bool>

可能还有更多我没有想到的。尽管在这些示例中,操作和 do 都有意义,但仍然与 不是一回事,因此需要单独的定义。a == ba != ba != b!(a == b)

评论

73赞 Oliver Charlesworth 6/14/2016
表达式构建是一个很好的实际例子,说明你什么时候需要这个,它不依赖于人为的场景。
7赞 6/14/2016
另一个很好的例子是向量逻辑运算。你宁愿通过一次数据计算,而不是两次通过计算。尤其是在你不能依赖编译器来融合循环的时代。或者即使在今天,如果你不能说服编译器,你的向量也不会重叠。!===!
43赞 Steve Jessop 6/14/2016
“你可能有表达式生成器对象”——那么运算符也可以构建一些表达式节点,到目前为止,我们仍然可以用 替换。同样如此,它可以返回。 更有说服力,因为例如,逻辑真实性取决于值是否存在,而不是值本身。!a != b!(a == b)lazy<bool>::operator!lazy<bool>optional<bool>boost::optional
44赞 jsbueno 6/14/2016
所有这些,还有 s - 请记住 s;NanNaN
10赞 Oliver Charlesworth 6/14/2016
@jsbueno:下面已经指出,NaN在这方面并不特别。
6赞 Dafang Cao 6/14/2016 #5
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);

我无法证明这个运算符重载是合理的,但在上面的例子中,不可能定义为 的“相反”。operator!=operator==

评论

1赞 AlainD 6/14/2016
@Snowman:大方并没有说这是一个好的枚举(也不是定义这样的枚举的好主意),它只是一个举例来说明一个观点。有了这个(也许是坏的)运算符定义,那么实际上并不意味着 的反义词。!===
1赞 6/14/2016
您@AlainD点击了我发布的链接,您知道该网站的目的吗?这就是所谓的“幽默”。
1赞 AlainD 6/15/2016
@Snowman: 我当然知道...对不起,我错过了这是一个链接,旨在讽刺!:o)
61赞 Trevor Hickey 6/14/2016 #6

但是,我担心的是,为什么需要两个单独的定义?

您不必同时定义两者。
如果它们是互斥的,则仍然可以通过仅定义 std::rel_ops 来简洁明了
==<

Fom cppreference:

#include <iostream>
#include <utility>

struct Foo {
    int n;
};

bool operator==(const Foo& lhs, const Foo& rhs)
{
    return lhs.n == rhs.n;
}

bool operator<(const Foo& lhs, const Foo& rhs)
{
    return lhs.n < rhs.n;
}

int main()
{
    Foo f1 = {1};
    Foo f2 = {2};
    using namespace std::rel_ops;

    //all work as you would expect
    std::cout << "not equal:     : " << (f1 != f2) << '\n';
    std::cout << "greater:       : " << (f1 > f2) << '\n';
    std::cout << "less equal:    : " << (f1 <= f2) << '\n';
    std::cout << "greater equal: : " << (f1 >= f2) << '\n';
}

是否有任何情况可以提出关于两个的问题 对象相等确实是有道理的,但问它们不是 平等没有意义?

我们经常将这些运算符与平等联系在一起。
尽管这是它们在基本类型上的行为方式,但没有义务这是它们在自定义数据类型上的行为。 如果您不想,您甚至不必返回布尔值。

我见过人们以奇怪的方式使运算符过载,结果发现这对他们特定于领域的应用程序是有意义的。即使接口显示它们是互斥的,作者也可能希望添加特定的内部逻辑。

(从用户的角度,或从实现者的角度)

我知道你想要一个具体的例子,
所以这里有一个我认为很实用的 Catch 测试框架

template<typename RhsT>
ResultBuilder& operator == ( RhsT const& rhs ) {
    return captureExpression<Internal::IsEqualTo>( rhs );
}

template<typename RhsT>
ResultBuilder& operator != ( RhsT const& rhs ) {
    return captureExpression<Internal::IsNotEqualTo>( rhs );
}

这些运算符正在做不同的事情,将一种方法定义为 !(不是)另一个。这样做的原因是为了让框架可以打印出所做的比较。为此,它需要捕获使用了哪些重载运算符的上下文。

评论

15赞 Daniel Jour 6/14/2016
天哪,我怎么可能不知道呢?非常感谢您指出这一点。std::rel_ops
6赞 T.C. 6/14/2016
来自 cppreferl(或其他任何地方)的近乎逐字的副本应明确标记并正确归属。 反正太可怕了。rel_ops
0赞 Trevor Hickey 6/14/2016
@T.C. 同意,我只是说这是 OP 可以采取的一种方法。我不知道如何解释rel_ops比所示示例更简单。我链接到它所在的位置,但发布了代码,因为参考页面总是可以更改。
4赞 T.C. 6/14/2016
您仍然需要明确代码示例 99% 来自 cppreference,而不是您自己的代码示例。
2赞 JDługosz 6/15/2016
Std::relops 似乎已经失宠了。查看提升操作,了解更有针对性的内容。
112赞 shrike 6/14/2016 #7

如果没有这种可能性,那么为什么 C++ 将这两个运算符定义为两个不同的函数呢?

因为你可以重载它们,通过重载它们,你可以赋予它们与原始含义完全不同的含义。

以运算符为例,最初是按位左移运算符,现在通常作为插入运算符重载,如 in ;与原来的意思完全不同。<<std::cout << something

因此,如果您接受运算符的含义在重载时会发生变化,那么就没有理由阻止用户给运算符赋予不完全运算符否定的含义,尽管这可能会令人困惑。==!=

评论

19赞 Sonic Atom 6/15/2016
这是唯一具有实际意义的答案。
2赞 nitro2k01 6/18/2016
在我看来,你的因果关系似乎是倒退的。您可以单独重载它们,因为它们作为不同的运算符存在。另一方面,它们可能不是作为不同的运算符存在的,因为您可以单独重载它们,而是由于遗留和便利性(代码简洁)的原因。==!=
44赞 Jander 6/14/2016 #8

有一些非常成熟的约定,其中 和 都是错误的,不一定是对立的。特别是,在 SQL 中,任何与 NULL 的比较都会产生 NULL,而不是 true 或 false。(a == b)(a != b)

如果可能的话,创建新的示例可能不是一个好主意,因为它太不直观了,但是如果您尝试对现有约定进行建模,那么可以选择让您的运算符在该上下文中“正确”运行是件好事。

评论

4赞 6/14/2016
在 C++ 中实现类似 SQL 的 null 行为?呜。但我想我认为它不应该在语言中被禁止,无论它可能多么令人反感。
1赞 Joe 6/14/2016
@dan1111 更重要的是,某些风格的 SQL 很可能是用 c++ 编码的,所以语言需要支持它们的语法,不是吗?
1赞 Benjamin Lindley 6/14/2016
如果我错了,请纠正我,我只是在这里离开维基百科,但与 SQL 中的 NULL 值进行比较不会返回 Unknown,而不是 False?对未知的否定不仍然是未知吗?因此,如果SQL逻辑是用C++编码的,您是否不想返回Unknown,并且还希望返回Unknown,并且希望返回.在这种情况下,实现为否定仍然是正确的。NULL == somethingNULL != something!UnknownUnknownoperator!=operator==
2赞 Benjamin Lindley 6/15/2016
@Barmar:嗯,不,这不是重点。OP已经知道这个事实,否则这个问题就不存在了。关键是要举一个例子,其中 1) 实现 or 中的一个而不是另一个是有意义的,或者 2) 以 否定 以外的方式实现。实现 NULL 值的 SQL 逻辑并非如此。operator==operator!=operator!=operator==
2赞 oulenz 6/15/2016
@dan1111我使用 SQL Server 和 BigQuery 的经验,并且肯定的评估结果为 ,而不是 .你可能会问,我怎么知道?a) 这些值显示为 ,而不是 b) 并且不计算为 ,是每个 SQL 程序员在某个时候都会学到的一课。事实上,我相信所有主要的 sql 实现都非常严格地遵循 sql 标准(一些迭代)。X == nullX != nullnullfalsenullfalsenot (X == null)not (X != null)true
12赞 Niall 6/15/2016 #9

作为对编辑的回应;

也就是说,如果某种类型可能具有运算符,但没有运算符,反之亦然,以及何时这样做是有意义的。==!=

一般来说,不,这没有意义。相等运算符和关系运算符通常以集合形式出现。如果存在平等,那么不平等也存在;小于,然后大于,依此类推。类似的方法也适用于算术运算符,它们通常也出现在自然逻辑集中。<=

std::rel_ops 命名空间证明了这一点。如果实现相等和小于运算符,则使用该命名空间将为您提供其他运算符,这些运算符是根据原始实现的运算符实现的。

总而言之,是否存在一个条件或情况,其中一个不会立即意味着另一个,或者不能根据其他条件实施?是的,可以说很少,但它们就在那里;同样,正如它自己的命名空间所证明的那样。出于这个原因,允许它们独立实现,可以让你利用语言来获取你需要或需要的语义,而这种语义对于代码的用户或客户端来说仍然是自然和直观的。rel_ops

前面提到的懒惰评估就是一个很好的例子。另一个很好的例子是给他们提供根本不意味着平等或不平等的语义。与此类似的示例是位移运算符,用于流插入和提取。虽然它可能在一般圈子里不受欢迎,但在某些特定领域,它可能是有道理的。<<>>

2赞 ToñitoG 6/15/2016 #10

也许是一个无与伦比的规则,哪里是错误的,并且像无状态位一样是错误的a != ba == b

if( !(a == b || a != b) ){
    // Stateless
}

评论

0赞 Thijser 6/15/2016
如果要重新排列逻辑符号,则 !( [美] ||[B]) 在逻辑上变为 ([!A]&[!B])
0赞 lorro 7/27/2016
请注意,和 的返回类型不一定是 ,如果您愿意,它们可能是一个包含无状态的枚举,但运算符可能仍被定义,因此成立。operator==()operator!=()bool(a != b) == !(a==b)
24赞 Centril 6/15/2016 #11

我只会回答你问题的第二部分,即:

如果没有这种可能性,那么为什么 C++ 将这两个运算符定义为两个不同的函数呢?

允许开发人员重载两者有意义的一个原因是性能。您可以通过同时实现 和 来允许优化。然后可能比现在便宜。一些编译器可能能够为您优化它,但可能无法,尤其是当您有涉及大量分支的复杂对象时。==!=x != y!(x == y)

即使在 Haskell 中,开发人员非常重视定律和数学概念,仍然允许重载两者和 ,正如你在这里看到的 (http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude.html#v:-61--61-):==/=

$ ghci
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
λ> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
        -- Defined in `GHC.Classes'

这可能被认为是微优化,但在某些情况下可能是必要的。

评论

3赞 Peter Cordes 6/15/2016
SSE (x86 SIMD) 包装类就是一个很好的例子。有一个 pcmpeqb 指令,但没有生成 != 掩码的打包比较指令。因此,如果你不能反转任何使用结果的逻辑,你必须使用另一条指令来反转它。(有趣的事实:AMD 的 XOP 指令集确实有 packed-compare。太糟糕了,英特尔没有采用/扩展 XOP;在那个即将消亡的 ISA 扩展中有一些有用的说明。neq
1赞 Peter Cordes 6/15/2016
首先,SIMD 的全部意义在于性能,您通常只需在对整体性能很重要的循环中手动使用它。将单个指令(所有指令用于反转比较掩码结果)保存在紧密循环中可能很重要。PXOR
0赞 Cheers and hth. - Alf 6/19/2016
当开销是一个逻辑否定时,性能作为原因是不可信的。
0赞 Centril 6/19/2016
如果计算成本明显高于 ,则可能是多个逻辑否定。由于分支预测等原因,计算后者可能会便宜得多。x == yx != y
5赞 Anirudh Sohil 6/15/2016 #12

最后,您使用这些运算符检查的是表达式 or 返回布尔值 ( 或 )。这些表达式在比较后返回布尔值,而不是互斥。a == ba != btruefalse

2赞 TOOGAM 6/18/2016 #13

通过自定义操作员的行为,您可以让他们做你想做的事。

您可能希望自定义内容。例如,您可能希望自定义一个类。只需检查特定属性即可比较此类的对象。知道是这种情况,您可以编写一些特定的代码来只检查最少的东西,而不是检查整个对象中每个属性的每一位。

想象一下,你可以发现某些东西是不同的,即使不是更快,也比你发现东西是一样的一样快。当然,一旦你弄清楚某件事是相同还是不同,那么你只需翻转一下就可以知道相反的情况。但是,翻转该位是一项额外的操作。在某些情况下,当代码被大量重新执行时,保存一个操作(乘以许多倍)可以提高整体速度。(例如,如果百万像素屏幕的每个像素保存一个操作,那么您刚刚保存了一百万个操作。乘以每秒 60 个屏幕,您可以节省更多操作。

HVD的回答提供了一些额外的例子。

7赞 It'sPete 6/19/2016 #14

强大的力量会带来巨大的责任感,或者至少是非常好的风格指南。

==并且可以超载以做任何您想做的事情。这既是祝福也是诅咒。不能保证这意味着.!=!=!(a==b)

2赞 oliora 6/30/2016 #15

是的,因为一个表示“等效”,另一个表示“非等同”,并且这些术语是相互排斥的。此运算符的任何其他含义都令人困惑,应尽可能避免。

评论

0赞 vladon 6/30/2016
它们并非在所有情况下都是相互排斥的。例如,两个无穷大既不相等又不相等。
0赞 oliora 6/30/2016
一般情况下,@vladon可以使用一个而不是另一个?不。这意味着它们并不相等。其余的都转到一个特殊函数,而不是运算符==/!=
0赞 oliora 6/30/2016
@vladon,请不要阅读我回答中的所有案例,而不是一般案例
0赞 nitro2k01 5/18/2018
@vladon 尽管这在数学中是正确的,但你能举一个例子,在 C 中由于这个原因不等于这个原因吗?a != b!(a == b)