==和!=相互依赖吗?

时间:2016-06-13 22:14:44

标签: c++ operator-overloading equality equality-operator

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

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

是否有任何可能的情况下,询问有关两个对象相等的问题是否有意义,但询问他们是否相等并不合理? (无论是从用户的角度,还是从实施者的角度来看)

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

15 个答案:

答案 0 :(得分:273)

a != b返回!(a == b)以外的内容时,您 希望语言自动将a == b重写为bool。你可以做到这一点有几个原因。

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

您可能进行了延迟评估,其中a == b没有并且不打算直接执行任何比较,而是返回某种可以转换为{lazy<bool>的{​​{1}} {1}}稍后隐式或明确地实际执行比较。可能与表达式构建器对象结合使用,以便在评估之前完成表达式优化。

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

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

答案 1 :(得分:110)

  

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

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

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

因此,如果您接受运算符的含义在重载时发生更改,那么就没有理由阻止用户赋予运算符==不正确的否定 of operator !=,虽然这可能令人困惑。

答案 2 :(得分: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';
}
  

是否有可能提出有关两个问题的问题   平等的对象确实有意义,但要求它们不存在   等于没有意义吗?

我们经常将这些运营商与平等联系起来 虽然这就是它们在基本类型上的行为方式,但没有义务这是它们在自定义数据类型上的行为。 如果你不想,你甚至不必返还一个bool。

我看到人们以奇怪的方式超载运营商,却发现它们对于特定领域的应用程序是有意义的。即使界面看起来表明它们是互斥的,作者也可能想要添加特定的内部逻辑。

  

(无论是从用户的角度,还是从实施者的角度来看)

我知道你想要一个具体的例子,
所以这是我认为实用的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 );
}

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

答案 3 :(得分:42)

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

如果可能的话,创建新的例子可能不是一个好主意,因为它太不直观了,但是如果你试图对现有的约定进行建模,那么它就是&#39很高兴有选择让你的操作员正确行事&#34;#34;对于那种情况。

答案 4 :(得分:23)

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

  

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

允许开发人员超载的一个原因是性能。您可以通过实施==!=来实现优化。然后x != y可能比!(x == y)便宜。有些编译器可能能够为您优化它,但也许不是,特别是如果您有涉及大量分支的复杂对象。

即使在Haskell中,开发人员非常重视法律和数学概念,仍然可以让==/=超载,如您所见(http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude.html#v:-61--61-):< / p>

$ 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'

这可能被认为是微观优化,但在某些情况下可能需要保证。

答案 5 :(得分:16)

  

是否有可能提出有关两个问题的问题   平等的对象确实有意义,但要求它们不存在   平等没有意义吗? (无论是从用户的角度来看,还是从   实施者的观点)

这是一个意见。也许它没有。但语言设计师并非无所不知,他们决定不限制可能会出现可能有意义的情况的人(至少对他们而言)。

答案 6 :(得分:13)

回应编辑;

  

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

general 中,不,它没有意义。平等和关系运营商通常都是成套的。如果有平等,那么不平等也是如此;使用<=等小于,然后大于等等。类似的方法也应用于算术运算符,它们通常也是自然逻辑集。

这在std::rel_ops命名空间中得到证明。如果您实现了等于和小于运算符,那么使用该命名空间可以为您提供其他服务,根据您原来实现的运算符实现。

所有人都说,是否有条件或情况,一方不会立即指另一方,或无法以其他方式实施?是的,有,可能很少,但他们在那里;再次,正如rel_ops作为自己的命名空间所证明的那样。因此,允许它们独立实现允许您利用该语言以对代码的用户或客户端仍然自然且直观的方式获取您需要或需要的语义。

已经提到的懒惰评估就是一个很好的例子。另一个很好的例子是赋予它们语义,这些语义并不意味着平等或不平等。与此类似的示例是用于流插入和提取的位移运算符<<>>。虽然在一般的圈子中可能不赞成,但在一些领域特定的领域,它可能是有意义的。

答案 7 :(得分:12)

如果0.25 ==运营商实际上并不意味着平等,那么与!=<<运营商不同的方式相同意味着位移。如果你把这些符号看作是一个其他概念,那么它们就不必互相排斥。

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

答案 8 :(得分:7)

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

==!=可以重载以执行您想要的任何操作。这既是一种祝福,也是一种诅咒。我无法保证!=表示!(a==b)

答案 9 :(得分:6)

liInput = [int(i) for i in lsInput if i.isdigit()]

我无法证明此运算符重载是正确的,但在上面的示例中,无法将enum BoolPlus { kFalse = 0, kTrue = 1, kFileNotFound = -1 } BoolPlus operator==(File& other); BoolPlus operator!=(File& other); 定义为operator!=的“反面”。

答案 10 :(得分:5)

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

答案 11 :(得分:4)

  

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

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

(我的例子是垃圾,但问题仍然存在,想想布隆过滤器,例如:如果某些东西,它们允许快速测试,但测试它是否&#39;可能会花费更多时间。)

  

[..]根据定义,javacv-bin/a != b

作为程序员,你有责任实现这一目标。编写测试可能是件好事。

答案 12 :(得分:2)

可能是一条无法比较的规则,其中a != b false a == b false 就像无状态位一样。

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

答案 13 :(得分:2)

通过自定义操作符的行为,您可以使它们按照您的需要进行操作。

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

想象一下这样一个案例:你可以发现某些事物的不同之处同样快,如果不是更快,那么你会发现某些东西是相同的。当然,一旦你弄清楚某些东西是相同还是不同,那么你只需稍微翻一下即可知道相反的情况。但是,翻转该位是一项额外的操作。在某些情况下,当代码重新执行很多时,保存一个操作(乘以很多次)可以提高整体速度。 (例如,如果你在百万像素屏幕的每个像素中保存一个操作,那么你只需要节省一百万次操作。每秒乘以60个屏幕,你可以节省更多的操作。)

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

答案 14 :(得分:2)

是的,因为一个意味着&#34;等同于&#34;和另一种手段&#34;非等同的&#34;这个词是相互排斥的。这个操作符的任何其他含义都令人困惑,应该尽一切可能避免。