==和!=相互依赖吗?

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

我知道我可以用一个定义另一个,但这不是我要问的。我也没有问通过值和单位来比较对象之间的区别。或者两个对象是否可以同时相等和不相等(这绝对不是一个选项!这些事情是相互排斥的)。我想问的是:

有没有可能在任何情况下,问关于两个对象相等的问题是有意义的,但问他们相等是没有意义的?(无论是从用户的角度,还是从实现者的角度)

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

24071 次浏览
有没有可能在任何情况下问关于两个的问题 对象相等是有意义的,但是问它们不相等 相等没有意义吗?(无论是从用户的角度,还是从 实现者的角度来看)< / p >

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

(. .为什么需要两个不同的定义?

需要考虑的一件事是,实现其中一个操作符可能比仅使用另一个操作符的反求值更有效。

(我在这里的例子是垃圾,但这一点仍然成立,想想bloom过滤器,例如:他们允许快速测试在一个集合中是否有,但测试它是否在可能需要更多的时间。)

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

这是你作为程序员的责任。这可能是编写测试的好方法。

如果==!=操作符实际上并不意味着相等,同样地,<<>>流操作符并不意味着位移位。如果你把这些符号当作其他概念来对待,它们就不必相互排斥。

就相等而言,如果您的用例保证将对象视为不可比较的,那么每次比较都应该返回false(或者如果操作符返回非bool类型,则返回不可比较的结果类型),那么这样做是有意义的。我想不出一个具体的情况,这将是必要的,但我可以看到它是足够合理的。

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

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

你可能会有惰性求值,其中a == b不会也不打算直接执行任何比较,而是返回某种类型的lazy<bool>,可以在以后的某个时间隐式或显式地转换为bool,以实际执行比较。可能与表达式构建器对象结合使用,以便在求值之前完成表达式优化。

你可能有一些自定义的optional<T>模板类,其中给定可选变量tu,你想允许t == u,但让它返回optional<bool>

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

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


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

我不能证明这个操作符重载是正确的,但在上面的例子中,不可能将operator!=定义为operator==的“相反”。

但我担心的是,为什么需要两个不同的定义呢?

你不必两个都定义 如果它们是互斥的,你仍然可以通过在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';
}
有没有可能在任何情况下问关于两个的问题 对象相等是有意义的,但是问它们不相等 相等没有意义吗?< / p >
我们经常将这些运算符与等式联系起来 尽管这是它们在基本类型上的行为方式,但没有义务在自定义数据类型上也是如此。 如果你不想,你甚至不需要返回bool值。< / p >

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

(无论是从用户的角度,还是从实现者的角度)

我知道你想要一个具体的例子,
所以这里有一个来自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 );
}

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

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

因为你可以让它们超载,通过超载,你可以赋予它们一个与原来完全不同的含义。

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

因此,如果你接受重载操作符时操作符的含义会发生变化,那么就没有理由阻止user给操作符==赋予一个不完全是操作符!=否定的含义,尽管这可能会令人困惑。

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

如果可能的话,创建这样的新示例可能不是一个好主意,因为这太不直观了,但是如果您试图对现有的约定建模,那么可以选择使您的操作符在该上下文中表现得“正确”。

作为对编辑的回应;

也就是说,如果某些类型可能具有操作符==而没有!=,或者反之亦然,以及在什么情况下这样做是有意义的。

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

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

这一切都说,在某些条件或情况下,其中一个并不立即意味着另一个,或者不能按照其他条件或情况实施?是的,有,可以说很少,但他们在那里;同样,rel_ops是它自己的命名空间。出于这个原因,允许它们独立实现可以让您利用语言来获得所需的语义,或者以一种对代码的用户或客户端来说仍然自然和直观的方式获得所需的语义。

前面提到的惰性求值就是一个很好的例子。另一个很好的例子是,给他们不等于或不等于的语义。一个类似的例子是位移位操作符<<>>用于流插入和提取。虽然在一般的圈子里它可能会引起不满,但在某些特定领域它可能是有意义的。

也许是一个不可比较的规则,其中a != b,而a == b,就像一个无状态位。

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

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

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

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

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

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

你可能希望自定义一些东西。例如,您可能希望自定义一个类。可以通过检查特定属性来比较该类的对象。了解了这种情况后,您可以编写一些只检查最小值的特定代码,而不是检查整个对象中每个属性的每一位。

想象这样一个例子,你可以很快地找出不同的东西,如果不是更快的话,就会发现相同的东西。当然,一旦你弄清楚某物是否相同或不同,那么你可以通过简单地翻转一点来知道相反的情况。然而,翻转该位是一个额外的操作。在某些情况下,当代码被多次重新执行时,节省一个操作(乘以许多次)可以提高整体速度。(例如,如果您在百万像素屏幕上每像素节省一个操作,那么您就节省了100万个操作。再乘以每秒60个屏幕,你就可以节省更多操作。)

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

权力越大,责任越大,或者至少是非常好的风格指南。

==!=可以被重载来做任何你想做的事情。这是福也是祸。不能保证!=就是!(a==b)

是的,因为一个意思是“等价的”,另一个意思是“不等价的”,这两个术语是相互排斥的。此操作符的任何其他含义都是令人困惑的,应该无论如何避免。