ChatGPT解决这个技术问题 Extra ChatGPT

== 和 != 是否相互依赖?

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

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

在任何情况下,询问两个对象是否相等确实有意义,但询问它们不相等是没有意义的? (无论是从用户的角度,还是从实施者的角度)

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

两个指针可能都为空,但不一定相等。
不知道这是否有意义,但读到这让我想到了“短路”问题。例如,可以定义 'undefined' != expression 始终为真(或假,或未定义),无论是否可以计算表达式。在这种情况下,a!=b 将根据定义返回正确的结果,但如果无法评估 b!(a==b) 将失败。 (或者如果评估 b 代价高昂,则需要花费大量时间)。
那么 null != null 和 null == null 呢?它可以是两者……所以如果 a != b 并不总是意味着 a == b。
来自 javascript (NaN != NaN) == true 的示例

佚名

a == b 返回的不是 bool 时,您希望语言自动将 a != b 重写为 !(a == b)。您可能会这样做有几个原因。

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

您可能有惰性求值,其中 a == b 不会也不打算直接执行任何比较,而是返回某种可以在以后隐式或显式转换为 boollazy<bool>进行比较。可能与表达式构建器对象结合使用,以允许在评估之前完成表达式优化。

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

可能还有更多我没有想到的。即使在这些示例中,操作 a == ba != b 都有意义,但 a != b 仍然与 !(a == b) 不同,因此需要单独定义。


表达式构建是一个很好的实际示例,说明您何时需要它,它不依赖于人为的场景。
另一个很好的例子是向量逻辑运算。您宁愿一次通过数据计算 != 而不是两次通过计算 == 然后 !。尤其是在你不能依赖编译器来融合循环的那一天。甚至在今天,如果您无法说服编译器您的向量不会重叠。
“您可能有表达式构建器对象”——那么运算符 ! 也可以构建一些表达式节点,我们仍然可以将 a != b 替换为 !(a == b),就目前而言。 lazy<bool>::operator! 也是如此,它可以返回 lazy<bool>optional<bool> 更有说服力,因为例如 boost::optional 的逻辑真实性取决于值是否存在,而不取决于值本身。
所有这些,还有 Nan - 请记住 NaN
@jsbueno:有人进一步指出,NaN 在这方面并不特别。
J
John Kugelman

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

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

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

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


这是唯一具有实际意义的答案。
在我看来,你的因果关系似乎倒退了。您可以分别重载它们,因为 ==!= 作为不同的运算符存在。另一方面,它们可能不作为不同的运算符存在,因为您可以单独重载它们,但由于遗留和方便(代码简洁)的原因。
T
Trevor Hickey

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

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

形式 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 testing framework 的一个我认为实用的例子:

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 );
}

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


天啊,我怎么知道 std::rel_ops?非常感谢您指出这一点。
来自 cppreference (或其他任何地方)的近乎逐字的副本应清楚地标记并正确归属。 rel_ops 无论如何都是可怕的。
@TC同意,我只是说它是OP可以采用的一种方法。我不知道如何解释 rel_ops 比显示的示例更简单。我链接到它的位置,但发布了代码,因为参考页面总是会改变。
您仍然需要明确说明代码示例 99% 来自 cppreference,而不是您自己的。
Std::relops 似乎已经失宠。查看 boost ops 以获得更有针对性的东西。
J
Jander

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

如果可能的话,创建新的例子可能不是一个好主意,因为它太不直观了,但是如果你试图对现有的约定进行建模,那么最好选择让你的操作符“正确”地表现语境。


在 C++ 中实现类似 SQL 的空行为?呜呜呜。但我认为这不是我认为应该在语言中被禁止的东西,无论它可能多么令人反感。
@dan1111 更重要的是,某些风格的 SQL 很可能用 C++ 编码,所以语言需要支持它们的语法,不是吗?
如果我错了,请纠正我,我只是在这里离开 wikipedia,但不与 SQL 中的 NULL 值比较返回未知,而不是 False?对未知的否定不还是未知吗?因此,如果 SQL 逻辑是用 C++ 编写的,您是否希望 NULL == something 返回 Unknown,并且您还希望 NULL != something 返回 Unknown,并且您希望 !Unknown 返回 Unknown。在那种情况下,将 operator!= 实现为 operator== 的否定仍然是正确的。
@Barmar:不,那不是重点。 OP已经知道这个事实,否则这个问题就不存在了。重点是提供一个示例,其中 1) 实现 operator==operator!= 之一,但不实现另一个,或 2) 以不同于 operator== 否定的方式实现 operator!= .为 NULL 值实现 SQL 逻辑并非如此。
@dan1111 根据我使用 sql server 和 bigquery 的经验,X == nullX != null 肯定会评估为 null,而不是 false。我怎么知道,你可能会问? a) 这些值显示为 null,而不是 false b) not (X == null)not (X != null) 不计算为 true,这是每个 sql 程序员在某个时候学到的一课。确实,我相信所有主要的sql 实现非常严格地遵守(一些迭代)sql 标准。
C
Centril

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

如果没有这种可能性,那么究竟为什么 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'

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


SSE (x86 SIMD) 包装类就是一个很好的例子。有一条 pcmpeqb 指令,但没有产生 != 掩码的压缩比较指令。因此,如果您不能仅反转使用结果的任何逻辑,则必须使用另一条指令来反转它。 (有趣的事实:AMD 的 XOP 指令集确实对 neq 进行了打包比较。太糟糕的是英特尔没有采用/扩展 XOP;在即将失效的 ISA 扩展中有一些有用的指令。)
首先,SIMD 的重点是性能,您通常只需要在对整体性能很重要的循环中手动使用它。在紧密循环中保存单个指令(PXOR 与全一以反转比较掩码结果)可能很重要。
当开销是一个逻辑否定时,性能作为一个原因是不可信的。
如果计算 x == y 的成本远高于 x != y,则可能不止一个逻辑否定。由于分支预测等原因,计算后者可能要便宜得多。
B
Benjamin Lindley

在任何情况下,询问两个对象是否相等确实有意义,但询问它们不相等是没有意义的? (无论是从用户的角度,还是从实施者的角度)

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


N
Niall

回应编辑;

也就是说,如果某些类型有可能有运算符 == 而不是 !=,反之亦然,什么时候这样做才有意义。

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

这在 std::rel_ops 命名空间中得到了证明。如果您实现相等和小于运算符,则使用该命名空间会为您提供其他命名空间,这些命名空间是根据您最初实现的运算符实现的。

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

已经提到的惰性评估就是一个很好的例子。另一个很好的例子是为它们提供根本不意味着相等或不相等的语义。与此类似的示例是用于流插入和提取的位移运算符 <<>>。尽管它在一般圈子中可能不受欢迎,但在某些特定领域的领域中它可能是有意义的。


T
Taywee

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

就相等而言,如果您的用例保证将对象视为不可比较的,那么每个比较都应返回 false(或不可比较的结果类型,如果您的运算符返回非布尔值),这可能是有意义的。我想不出有必要这样做的具体情况,但我认为这是足够合理的。


d
dippas

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

==!= 可以重载以做任何你想做的事。这既是祝福,也是诅咒。不能保证 != 表示 !(a==b)


D
Dafang Cao
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

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

我不能证明这个运算符重载是合理的,但在上面的示例中,不可能将 operator!= 定义为 operator== 的“对立面”。


@Snowman:大方并没有说它是一个好的枚举(也不是定义这样的枚举的好主意),它只是一个说明一点的例子。有了这个(可能是坏的)运算符定义,那么 != 确实不意味着 == 的反义词。
@AlainD 您是否单击了我发布的链接,并且您知道该网站的目的吗?这就是所谓的“幽默”。
@Snowman:我当然会......对不起,我错过了它是一个链接,并具有讽刺意味! :o)
A
Azeem

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


D
Daniel Jour

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

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

(我这里的例子很垃圾,但重点仍然存在,想想布隆过滤器,例如:如果某物不在集合中,它们允许快速测试,但测试它是否在可能需要更多时间。)

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

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


!((a == rhs.a) && (b == rhs.b)) 如何不允许短路?如果是 !(a == rhs.a),则不会评估 (b == rhs.b)
不过,这是一个不好的例子。短路在这里并没有增加任何神奇的优势。
@Oliver Charlesworth 单独它不会,但是当与单独的运算符结合时,它会:在 == 的情况下,一旦第一个对应元素不相等,它将停止比较。但是在 != 的情况下,如果它是根据 == 实现的,则需要首先比较所有相应的元素(当它们都相等时)才能判断它们不是不相等的: P 但是当按照上面的例子实现时,它会在找到第一个不相等的对时立即停止比较。确实很好的例子。
@BenjaminLindley 是的,我的例子完全是胡说八道。不幸的是,我无法想出另一个自动取款机,现在为时已晚。
@BarbaraKwarc:!((a == b) && (c == d))(a != b) || (c != d) 在短路效率方面是等效的。
T
TOOGAM

通过自定义操作员的行为,您可以让他们为所欲为。

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

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

hvd's answer 提供了一些其他示例。


o
oliora

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


它们并非对所有情况都相互排斥。例如,两个无穷大既互不相等也不互不相等。
@vladon 在一般情况下可以使用一种而不是另一种吗?不,这意味着他们只是不平等。其余的都交给一个特殊的函数而不是 operator==/!=
@vladon 请阅读我的回答中的所有案例,而不是一般案例。
@vladon 尽管这在数学中是正确的,但你能举一个例子,因为这个原因,在 C 中 a != b 不等于 !(a == b) 吗?
A
Azeem

也许是一个无法比较的规则,其中 a != bfalsea == bfalse 就像一个无状态位。

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

如果要重新排列逻辑符号,则 !( [A] || [B]) 逻辑上变为 ([!A]&[!B])
请注意,operator==()operator!=() 的返回类型不一定是 bool,如果您愿意,它们可能是包含无状态的枚举,但仍可能定义运算符,因此 (a != b) == !(a==b) 成立。