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


292

我正在学习C ++中的运算符重载,我看到了==!=它们只是一些可以针对用户定义的类型进行自定义的特殊功能。但是,我担心的是,为什么需要两个单独的定义?我认为如果a == b为true,则a != b自动为false,反之亦然,并且没有其他可能性,因为根据定义,a != b!(a == b)。而且我无法想象任何情况都不是真的。但是也许我的想象力有限,或者我什么都不知道?

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

是否有可能问到关于两个对象相等的问题是有意义的,而问他们相等是没有意义的呢?(从用户角度或从实施者角度)

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


13
两个指针都可以为null,但不一定相等。
阿里Caglayan

2
不确定在这里是否有意义,但是阅读此内容使我想到了“短路”问题。例如,'undefined' != expression无论是否可以对表达式求值,都可以定义始终为true(或false或undefined)。在这种情况下,a!=b将根据定义返回正确的结果,但是!(a==b)如果b无法评估,将失败。(或者,如果评估b成本很高,则需要花费很多时间)。
Dennis Jaheruddin

2
那么null!= null和null == null呢?两者都可以...因此,如果a!= b并不总是意味着a == b。
zozo

4
来自javascript的示例(NaN != NaN) == true
chiliNUT

Answers:


272

希望该语言自动重写a != b!(a == b)a == b比其他收益东西bool。并且有一些原因使您可以做到这一点。

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

您可能有一个懒惰的评估,在这里a == b不打算也不打算直接执行任何比较,而是返回某种lazy<bool>可以bool在以后的某个时间隐式或显式转换为实际执行比较的结果。可能与表达式构建器对象结合使用,以允许在评估之前进行完整的表达式优化。

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

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


72
表达式构建是一个理想的实用示例,说明您何时需要此表达式,它不依赖人为设计的场景。
奥利弗·查尔斯沃思

6
另一个很好的例子是矢量逻辑运算。你宁愿一次通过数据计算!=的,而不是两道计算==!。特别是在您无法依靠编译器融合循环的那一天。甚至在今天,如果您无法说服编译器,向量也不会重叠。

41
“你可能有表达式生成器对象” -好那么运营商!也可以建立一些表达节点,我们仍在替换a != b!(a == b),只要那去。同样lazy<bool>::operator!,它可以返回lazy<bool>optional<bool>更具说服力,因为例如逻辑上的真实性boost::optional取决于值是否存在,而不取决于值本身。
史蒂夫·杰索普

42
所有这些以及Nans-请记住 NaNs;
jsbueno

9
@jsbueno:有人进一步指出,NaN在这方面并不特殊。
奥利弗·查尔斯沃思

110

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

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

以operator为例,<<最初是按位左移运算符,现在通常像in一样重载为插入运算符std::cout << something。与原始含义完全不同。

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


18
这是唯一具有实际意义的答案。
Sonic Atom

2
在我看来,您似乎有因果关系倒退。您可以分别重载它们,因为==!=作为不同的运算符存在。另一方面,它们可能不作为单独的运算符存在,因为您可以分别重载它们,但是由于传统和方便(代码简洁)原因。
nitro2k01

60

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

您不必同时定义两者。
如果它们是互斥的,那么您仍然可以通过仅定义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 );
}

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


14
哦,我,我怎么知道std::rel_ops?非常感谢您指出这一点。
Daniel Jour

5
来自cppreference(或其他任何地方)的近普通副本应加以清楚标记并正确归因。rel_ops反正很恐怖。
TC

@TC同意,我只是说OP可以采用的一种方法。我不知道如何比显示的示例更简单地解释rel_ops。我链接到了它的位置,但是由于参考页可能始终会更改,因此会发布代码。
Trevor Hickey

4
您仍然需要弄清楚代码示例是cppreference的99%,而不是您自己的。
TC

2
Std :: relops似乎已失宠。请查看增强操作,以获取更具针对性的内容。
JDługosz

43

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

如果可能的话,尽可能地创建一个新的示例可能不是一个好主意,因为它很不直观,但是如果您要对现有的约定进行建模,那么可以选择让操作员对此“正确地表现”是很好的选择。上下文。


4
在C ++中实现类似SQL的空行为?www 但是我想这不是我认为应该禁止的语言,无论它多么令人讨厌。

1
@ dan1111更重要的是,某些SQL风格很可能是用c ++编写的,因此该语言需要支持其语法,不是吗?

1
如果我错了,请纠正我,我只是在这里退出维基百科,但是不与SQL中的NULL值进行比较返回Unknown,不是False吗?而且对未知的否定还不是未知的吗?因此,如果SQL逻辑是用C ++编写的,您是否不想NULL == something返回Unknown,也不想NULL != something返回Unknown,而又想!Unknown返回Unknown。在那种情况下,实施operator!=否定operator==仍然是正确的。
本杰明·林德利

1
@Barmar:好的,但是那怎么使语句“ SQL NULLs这样工作”正确呢?如果我们将比较运算符实现限制为返回布尔值,这是否仅意味着使用这些运算符实现SQL逻辑是不可能的?
本杰明·林德利

2
@Barmar:好吧,那不是重点。OP已经知道这一事实,否则这个问题将不存在。关键是要提供一个示例,在该示例中,要么1)实施一个operator==operator!=,但另一个都不有意义,要么2)以operator!=除否定的方式实施operator==。而且,为NULL值实现SQL逻辑不是这种情况。
本杰明·林德利

23

我只会回答您问题的第二部分:

如果没有这种可能性,那么为什么在地球上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
SSE(x86 SIMD)包装器类就是一个很好的例子。有一条pcmpeqb指令,但没有打包比较指令产生!=掩码。因此,如果您不能仅仅颠倒任何使用结果的逻辑,就必须使用另一条指令将其取反。(有趣的事实:AMD的XOP指令集确实具有针对包装的比较neq。太糟糕了,英特尔没有采用/扩展XOP;在即将死的ISA扩展中有一些有用的指令。)
Peter Cordes

1
首先,SIMD的要点是性能,您通常只需要在对整体性能至关重要的循环中手动使用它。PXOR在紧凑的循环中保存一条指令(用全1取反比较掩码结果)可能很重要。
彼得·科德斯

当开销是一种逻辑否定时,作为性能的原因是不可信的。
干杯和健康。-Alf

如果计算x == y成本比实际成本高得多,那可能不止一个逻辑否定x != y。计算后者可能是由于分支预测等显著便宜
Centril

16

是否有可能问到关于两个对象相等的问题是有意义的,而问他们不相等是没有意义的呢?(从用户角度或从实施者角度)

那是一个意见。也许不是。但是语言设计人员并非全知,决定不限制可能提出合理建议的人(至少对他们而言)。


13

响应编辑;

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

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

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

综上所述,是否存在一个条件或情况,其中一个不会立即意味着另一个,或者无法就另一个实施?是的,可以说是很少,但是他们在那里。再次,如rel_ops了自己是一个名称空间。因此,允许它们独立实现可以使您利用该语言来获得所需或所需要的语义,这种方式对于代码的用户或客户而言仍然是自然而直观的。

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


12

如果==and !=运算符实际上并不暗示相等,则与<<and >>流运算符并不暗示位移相同。如果将符号当作其他含义使用,则它们不必相互排斥。

就相等性而言,如果用例保证将对象视为不可比较的对象,则是有意义的,因此每个比较都应返回false(如果运算符返回非布尔值,则返回比较结果类型)。我无法想到有什么必要的具体情况,但我可以看到它足够合理。


7

强大的能力会负责任地发挥作用,或者至少是非常好的样式指南。

==并且!=可以超负荷执行您想要的任何操作。这既是福也是祸。没有保证!=意味着!(a==b)


6
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

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

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



1
@Snowman:大方并没有说它是一个很好的枚举(定义这样的枚举也不是一个好主意),它只是一个说明问题的例子。使用此(可能是错误的)运算符定义,那么!=确实并不意味着与的相反==
AlainD

1
@AlainD您是否单击了我发布的链接,您知道该网站的目的吗?这被称为“幽默”。

1
@Snowman:我当然会...对不起,我错过了它的链接,意在讽刺!:o)
AlainD

等一下,你是一元超载==吗?
LF


4

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

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

(我的示例在这里是垃圾,但问题仍然存在,例如,考虑使用bloom过滤器:如果包含某些内容,它们可以快速测试,但是如果要放入其中,则可能需要花费更多时间。)

[..]的定义a != b!(a == b)

作为程序员,这是您的责任。编写测试可能是一件好事。


4
怎么!((a == rhs.a) && (b == rhs.b))不允许短路?如果为!(a == rhs.a),则(b == rhs.b)不会进行评估。
本杰明·林德利

但是,这是一个不好的例子。短路在这里没有神奇的优势。
奥利弗·查尔斯沃思

@Oliver Charlesworth Alone不会,但是当与单独的运算符结合使用时,它会:如果是==,则在第一个对应元素不相等时,它将停止比较。但是在情况下!=,如果是根据实现的==,则需要先比较所有对应的元素(当它们相等时),以判断它们不是不相等的:P但是按如下方式实现在上面的示例中,它将在找到第一个非相等对后立即停止比较。确实是一个很好的例子。
BarbaraKwarc '16

@BenjaminLindley是的,我的例子完全是胡说八道。不幸的是,我无法提出另一个atm,在这里为时已晚。
Daniel Jour

1
@BarbaraKwarc:!((a == b) && (c == d))(a != b) || (c != d)在短路效率方面是等效的。
奥利弗·查尔斯沃思

2

通过自定义操作员的行为,可以使他们做自己想要的事情。

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

想象一下这样一种情况,您可以发现某些不同之处与发现某些相同之处一样快(如果不是更快)。当然,一旦弄清楚某个事物是相同还是不同,那么只需翻转一下就可以知道相反的事物。但是,翻转该位是额外的操作。在某些情况下,当大量代码重新执行时,节省一个操作(乘以多次)可以总体上提高速度。(例如,如果您在一个百万像素屏幕的每个像素上保存一个操作,那么您只保存了一百万个操作。乘以每秒60个屏幕,则可以保存更多的操作。)

hvd的答案提供了一些其他示例。


2

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


它们并非在所有情况下都是互斥的。例如,两个无穷都彼此不相等且彼此不相等。
vladon

@vladon可以在一般情况下使用另一种方法吗?不,这意味着它们不相等。其余的全部去一个特殊的函数而不是运算符== /!=
oliora

请@vladon,而不是一般情况,请阅读我的回答中的所有情况
oliora

@vladon尽管在数学上确实如此,您是否可以给出一个示例,在其中a != b为什么不等于!(a == b)
nitro2k01

2

也许不可比的规则,即a != b虚假的a == b假的就像无状态的一样。

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

如果您想重新排列逻辑符号,那么!([A] || [B])在逻辑上成为([!A]&[!B])
Thijser

请注意,operator==()and 的返回类型operator!=()不一定是bool,如果您愿意,它们可能是一个包含无状态的枚举,但是仍然可以定义运算符,使其(a != b) == !(a==b)成立
。.– lorro
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.