我正在学习C++中运算符重载,并且我看到EDCOX1 0和EDCOX1 1是简单的,可以为用户定义的类型定制的特殊功能。不过,我担心的是,为什么需要两个单独的定义?我认为,如果a == b是真的,那么a != b自动是假的,反之亦然,并且没有其他可能性,因为根据定义,a != b是!(a == b)。我无法想象在任何情况下这是不真实的。但也许我的想象力有限,或者我对什么一无所知?
我知道我可以用另一个定义一个,但这不是我要问的。我也不想问按价值或身份比较对象之间的区别。或者两个对象是否可以同时相等和不相等(这绝对不是一个选项!这些东西是相互排斥的)。我想问的是:
在任何情况下,询问两个物体相等是否有意义,但询问两个物体不相等是否有意义?(无论是从用户的角度,还是从实施者的角度)
如果没有这样的可能性,那么为什么地球上C++将这两个操作符定义为两个不同的函数?
- 评论不用于扩展讨论;此对话已被移动到聊天。
- 两个指针都可以为空,但不一定相等。
- 不确定这是否有意义,但阅读这篇文章让我想到了"短路"问题。例如,可以定义'undefined' != expression始终为真(或假,或未定义),而不管表达式是否可以计算。在这种情况下,根据定义,a!=b将返回正确的结果,但如果无法对b进行评估,!(a==b)将失败。(或者,如果评估b很昂贵,则需要花费大量时间)。
- 那空的呢!=null和null==null?两者都可以…所以如果A!=b并不总是意味着a==b。
- 最简单的表达方式(太短了,不可能是一个答案,IMO)是,虽然一个通常是用另一个来定义的,但这并不是强制性的,因为总有可能你会遇到一个奇怪的情况,你不希望他们内在地联系在一起。标准允许您自由地以最适合这种情况的方式单独定义它们,以防止出现这种情况时程序员受到限制。这也是为什么你可以自由地定义,例如,如果你愿意的话,用operator-=而不是operator+=来定义operator+。
- (并不意味着任何人实际上是根据operator-=来定义operator+,只是允许你这样做,即使operator+是根据operator+=来定义的。)
- javascript (NaN != NaN) == true中的一个例子
- 对于无法比较的对象,您如何定义==和!=。在这种情况下,两者都应该是错误的。
- @alicaglayan"同一类型的两个空指针值应比较相等。"[conv.ptr]p1。或者你说的那个,他们的位模式是什么意思?
- E/IS/C++Real/EXPR.EQ(2)
- "CHILNUUT"不是"来自JavaScript",它在大多数语言中都是真实的,包括C++
当a == b返回的不是bool时,您不希望该语言自动将a != b重写为!(a == b)。你为什么要这么做,有几个原因。
您可能有Expression Builder对象,其中a == b不执行任何比较,也不打算执行任何比较,只是构建一些表示a == b的表达式节点。
您可能会有懒惰的评估,其中a == b不直接执行任何比较,也不打算直接执行任何比较,而是返回某种类型的lazy,可以在以后某个时间隐式或显式转换为bool,以实际执行比较。可能与表达式生成器对象结合在一起,以便在计算之前进行完整的表达式优化。
您可能有一些自定义的optional模板类,在给定可选变量t和u的情况下,您希望允许t == u但使其返回optional。
可能还有更多我没想到的。尽管在这些例子中,a == b和a != b的操作都是有意义的,但a != b与!(a == b)并不是一回事,因此需要单独的定义。
- 表达式构建是一个非常好的实际例子,可以说明您何时需要它,而不依赖于人为的场景。
- 另一个很好的例子是向量逻辑运算。你宁愿一次通过数据计算!=,而不是两次通过计算==,然后是!。尤其是当你不能依靠编译器来融合循环的时候。或者即使在今天,如果你不能说服编译器,你的向量也不会重叠。
- "您可能有表达式生成器对象"--那么,操作符!也可以构建一些表达式节点,我们仍然可以用!(a == b)替换a != b,就目前而言。同样适用于lazy::operator!,它可以返回lazy。optional更具说服力,因为例如boost::optional的逻辑真实性取决于某个值是否存在,而不是取决于该值本身。
- 所有这些,还有Nans—请记住Nans;
- @JSBUENO:有人进一步指出,在这方面,NAN并不特别。
- @撇开hvd表达式不谈,如何(a!=b)与…不同!(a==b)?
- @Stevejessop对于表达式生成器对象,a != b和!(a == b)不一定是等效的。您可能需要原始表达式的忠实表示,或者表达式本身可能表示其他异常之一。对于lazy,我想到的实现不是重载操作符,而是隐式转换为T。您正在考虑的实现也是可能的,但是假设您将它扩展到除!之外的其他运算符,这基本上意味着您将再次构建表达式。
- @我希望我对史蒂夫·杰索普的反应能更清楚。在我心目中的lazy中,a == b会返回lazy。应用!会阻塞,直到其值已知为止,然后求反该值。不过,其他方法也完全有效。
- 这是一个很好的例子,说明了为什么伟大的程序员往往会造就糟糕的数学家(反之亦然)。尊重程序员。
- @hvd i不是指自动将a != b重写为!(a==b)的语言,但两个表达式的结果是等效的。不过,我接受了你的回答,因为我睁开眼睛说,==和!=可能过载,返回的是与bool不同的类型。将它们用作表达式树构建器听起来很酷:)但问题仍然存在:即使我将它们用作表达式构建器,难道我仍然需要超载两者吗,因为只有一个没有意义吗?
- @在我的例子中,两个表达式的结果不一定是等价的。:)是的,我认为我同意让一个操作员工作而不是让另一个操作员工作是没有意义的,或者至少如果有一种情况是有意义的,我不能去想它。
- @Hurkyl:ModernFixedSize的短向量simd:Agner Fog的SSE/AVX包装类库为比较运算符返回一个布尔向量结果。(矢量同__m128i中的矢量,而不是std::vector中的矢量)。所以有类似于static inline Vec16cb operator == (Vec16c const & a, Vec16c const & b) { return _mm_cmpeq_epi8(a,b); }的函数,其中Vec16c用16个1字符元素的类包装__m128i,Vec16cb是元素为0或-1的子类。
- @Barbarakwarc:对于SSE整数向量,可以有效地实现operator ==,但不能实现operator !=。SSE为不同的元素大小提供了pcmpeq指令,但只有AMD的XOP扩展提供了不相等的比较。如果没有这个,你能做的最好!=是两条指令:pcmpeqb,然后将压缩比较结果中的所有位与矢量为所有位的xor进行翻转。(或者与所有零的向量进行另一个比较,因此0->-1和-1->0)。所以这是两个指令加上一个全一向量常数。
- 通常,您可以颠倒下一步所做的任何操作的逻辑,例如将参数反转为混合指令。这是不提供operator !=的一个正当理由。Agner Fog的VCL确实提供了一个,让程序员记住,!=速度较慢,在可能的情况下应该避免,但不这样做是一个有效的设计选择,如果用户真的需要相反的版本,则强制库的用户编写!(a == b)。或者仅在为支持AMD XOP扩展的目标构建时提供!=。(最后一个不是一个好的设计选择,但有效。)
- 另外,三值逻辑(如SQL)与NULL一起使用(尽管这在optional示例中有所介绍)。
- 无穷大不能正常比较
- 我无法想象我想要的东西比图书馆/班级少,给我一个与a == b和!(a != b)不同的结果。如果不能遵循基本逻辑规则,就不应该重载这个运算符。创建一个特殊的功能或其他什么,但请不要添加这么大的混乱…
If there is no such possibility, then why on Earth does C++ have these two operators being defined as two distinct functions?
因为你可以让他们超载,通过超载他们,你可以给他们一个完全不同于他们原来的意义。
例如,操作符<<,原来是位左移位操作符,现在通常作为插入操作符重载,就像在std::cout << something中一样;与原来的操作符完全不同。
因此,如果您接受当您重载某个操作符时它的含义会发生变化,那么就没有理由阻止用户向操作符==赋予一个含义,这并不完全是对操作符!=的否定,尽管这可能会令人困惑。
- 这是唯一有实际意义的答案。
- 在我看来,你的因果关系似乎是颠倒的。您可以单独重载它们,因为==和!=作为不同的运算符存在。另一方面,它们可能不作为不同的操作符存在,因为您可以单独重载它们,但这是由于遗留和方便(代码简洁)的原因。
My concern is, though, why are there two separate definitions needed?
你不必定义两者。如果它们是互斥的,那么您仍然可以通过在std::rel-ops旁边定义==和<来进行简洁的定义。
FOM CPP参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #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) << '
';
std::cout <<"greater: :" << (f1 > f2) << '
';
std::cout <<"less equal: :" << (f1 <= f2) << '
';
std::cout <<"greater equal: :" << (f1 >= f2) << '
';
} |
Is there any situation possible in which asking questions about two
objects being equal does make sense, but asking about them not being
equal doesn't make sense?
我们经常把这些算符与相等联系起来。尽管这是它们在基本类型上的行为方式,但没有义务将其作为自定义数据类型上的行为。如果你不想的话,你甚至不必还一个乳房。
我看到过人们以奇怪的方式重载操作符,结果发现这对于他们特定于领域的应用程序是有意义的。即使界面显示它们是互斥的,作者也可能希望添加特定的内部逻辑。
(either from the user's perspective, or the implementer's perspective)
我知道你想要一个具体的例子,下面是我认为实用的catch测试框架中的一个:
1 2 3 4 5 6 7 8 9
| 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 );
} |
这些操作符正在做不同的事情,将一个方法定义为一个方法是没有意义的!(不是)另一个。这样做的原因是,框架可以打印出所做的比较。为了做到这一点,它需要捕获所使用的重载运算符的上下文。
- 哦,天哪,我怎么可能不知道关于std::rel_ops?非常感谢您指出这一点。
- CPP参考文件(或其他任何地方)的近逐字副本应清楚标记并适当归属。不管怎样,rel_ops是可怕的。
- @T.C.同意了,我只是说这是一个操作可以采用的方法。我不知道如何比所示的例子更简单地解释关系。我链接到了它的位置,但是由于引用页总是可以更改,所以发布了代码。
- 您仍然需要清楚地说明代码示例是来自cppreference的99%,而不是您自己的。
- @T.C.OK,添加了一个CPP引用,尽管代码是基本的。
- @很好,在语言中,==和!=可能没有"物理"的比较义务,但是仍然有"常识"的义务:重新定义它们去做一些比比较更重要的事情,会破坏它们的语义,使它们无法直观地使用,因为这会破坏用户的期望和需求。让他们翻阅手册,找出他们对特定上下文的意义,这听起来不好笑。
- @关于catch框架的trevorHickey,是的,听起来很酷;)但是有一个……捕获:p它仍然将==和!=定义为互相补充的意思。我好奇的是,在某些情况下,定义==而不是!=(或相反)对于某种类型是有意义的。
- std::再贷款似乎失宠了。查看Boost Ops了解更具针对性的内容。
- 我也不知道江户十一〔六〕的事。对于我的一个老问题,这将是一个极好的答案。
有一些非常成熟的约定,其中(a == b)和(a != b)都是both false不一定是对立的。尤其是在SQL中,与空值的任何比较都会产生空值,而不是真值或假值。
如果可能的话,创建新的示例可能不是一个好主意,因为这是非常不具说服力的,但是如果你试图建立一个现有的约定模型,那么让你的操作员在这种情况下"正确地"工作是很好的选择。
- 这不是真的(至少对南来说)。
- 在C++中实现类似SQL的空行为?EWWW。但我想这不是我认为应该禁止的语言,无论它可能是令人不快的。
- 更重要的是,一些口味的SQL可能被编码在C++中,所以语言需要支持它们的语法,不是吗?
- 如果我错了,请纠正我,我只是在这里离开维基百科,但不能与SQL中的空值进行比较返回unknown,而不是false?对未知的否定不是仍然未知吗?因此,如果SQL逻辑在C++中被编码,那么你不希望EDCOX1 OR 9返回未知,并且你也希望EDCOX1×10 }返回未知,并且你希望EDCOX1 OR 11返回EDCOX1×12。在这种情况下,实施operator!=作为operator==的否定仍然是正确的。
- @Benjaminly所讨论的上下文是一个返回布尔值的重载。C没有布尔型Unknown,所以他们返回一个值,这个值本身就是它的倒数的想法并不能真正计算出来。
- @Barmar:好吧,那这又如何使"SQL空值以这种方式工作"的语句正确呢?如果我们将比较运算符实现限制为返回布尔值,这是否意味着用这些运算符实现SQL逻辑是不可能的?
- @Benjaminly在有条件的上下文中使用时,Unknown实际上与false相同。因此,if a = null和if a != null都失败了。
- @巴玛:我认为任何实现这种逻辑的库都是经过精心设计的。如果有人真的想通过C++运算符重载来实现这个SQL逻辑,他们将使用一个三值逻辑,就像SQL一样。
- @Benjaminly的观点是,语言不限制您仅对具有简单二进制逻辑的对象使用==和!=。
- @巴玛:不,这不是重点。手术室已经知道这个事实,否则这个问题就不存在了。重点是提供一个有意义的例子:1)执行operator==或operator!=中的一个,但不执行另一个;2)以除否定operator==以外的方式执行operator!=。而实现空值的SQL逻辑并不是这种情况。
- @我很高兴你能很好地理解我的问题。你能对我的问题提出任何改进意见,以便其他人也能更好地理解它,并消除可能出现的任何困惑吗?
- @dan1111根据我在SQL Server和BigQuery方面的经验,X == null和X != null最肯定是对null进行评估,而不是对false进行评估。你可以问,我怎么说?a)这些值显示为null而不是falseb)not (X == null)和not (X != null)不对true进行评估,是每个SQL程序员在某个时刻学到的一个教训。事实上,我相信所有主要的SQL实现都非常接近(某些迭代)SQL标准。
- @乌伦兹,哇,我不知道!谢谢你的纠正。
- 真的谢谢大家!我已经更正了我的答案。
我只回答你问题的第二部分,即:
If there is no such possibility, then why on Earth does C++ have these two operators being defined as two distinct functions?
允许开发人员过载这两种情况有意义的一个原因是性能。您可以通过实现==和!=来允许优化。那么,x != y可能比!(x == y)便宜。有些编译器可能能够为您优化它,但也许不能,特别是当您有复杂的对象,并且涉及很多分支时。
即使在haskell,开发人员非常重视法律和数学概念,仍然允许一个超负荷的==和/=,如您所见(http://hackage.haskell.org/package/base-4.9.0.0/docs/prelude.html-v:-61--61-):
1 2 3 4 5 6 7
| $ 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的打包比较。太糟糕了,Intel没有采用/扩展XOP;其中有一些有用的指令很快就会失效。
- 首先,SIMD的关键是性能,通常只需要在对整体性能很重要的循环中手动使用它。在一个紧密循环中保存一条指令(PXOR和所有指令以反转比较掩码结果)很重要。
- 当开销是一个逻辑否定时,性能作为一个原因是不可信的。
- 如果计算x == y的成本比x != y的成本更高,那么可能会有不止一个逻辑否定。由于分支预测等原因,计算后者可能会便宜很多。
Is there any situation possible in which asking questions about two
objects being equal does make sense, but asking about them not being
equal doesn't make sense? (either from the user's perspective, or the
implementer's perspective)
这是一种意见。也许不是这样,但是语言设计者,不是无所不知的,决定不限制那些可能想出可能有意义的情况的人(至少对他们来说)。
响应编辑;
That is, if it is possible for some type to have the operator == but not the !=, or vice versa, and when does it make sense to do so.
一般来说,不,这没有意义。等式和关系运算符通常以集合形式出现。如果存在相等,那么也存在不等式;对于<=等,小于、大于等。同样,算术运算符也采用类似的方法,它们通常也是自然逻辑集。
这在std::rel_ops名称空间中得到了证明。如果您实现了相等和小于运算符,那么使用该名称空间将为您提供其他名称空间,这些名称空间是根据原始实现的运算符实现的。
所有人都说,是否有条件或情况,其中一个不会立即意味着另一个,或不能执行的其他方面?是的,可以说是很少的,但它们确实存在;同样,正如在rel_ops中所证明的那样,它们本身就是一个名称空间。出于这个原因,允许它们独立地实现允许您利用语言来获得您所需要的语义,对于代码的用户或客户机来说,这种方式仍然是自然和直观的。
上面提到的懒惰的评估就是一个很好的例子。另一个很好的例子是给他们提供了语义,这并不意味着平等或完全平等。与此类似的例子是用于流插入和提取的位移位操作符<<和>>。虽然在一般的圈子里人们可能不喜欢它,但在某些领域,它可能是有意义的。
如果==和!=运算符实际上并不意味着相等,那么,与<<和>>流运算符不意味着位移动相同。如果你把这些符号看作是其他概念的意思,它们就不必互相排斥。
在相等性方面,如果用例保证将对象视为不可比较的对象,那么每个比较都应该返回false(如果运算符返回非bool,则返回不可比较的结果类型),这是有意义的。我想不出一个具体的情况,这是可以保证的,但我可以看到它是合理的足够。
强大的力量带来巨大的责任感,或者至少是很好的风格指南。
==和!=可以超载,可以做任何你想做的事情。这既是一种祝福,也是一种诅咒。不能保证!=表示!(a==b)。
1 2 3 4 5 6 7 8
| enum BoolPlus {
kFalse = 0,
kTrue = 1,
kFileNotFound = -1
}
BoolPlus operator==(File& other);
BoolPlus operator!=(File& other); |
我不能证明这个操作符重载的合理性,但是在上面的示例中,不可能将operator!=定义为operator==的"相反"。
- 为什么要定义这样的枚举?
- @雪人:大丰没有说它是一个很好的枚举(也不是像这样定义一个枚举的好主意),它只是一个例子来说明一点。有了这个(可能是坏的)操作符定义,那么!=实际上并不意味着与==相反。
- @你是否点击了我发布的链接,你知道这个网站的目的吗?这叫做"幽默"。
- @雪人:我当然知道……对不起,我错过了这是一个链接,意在讽刺!o)
最后,使用这些运算符检查的是表达式a == b或a != b返回布尔值(true或false)。这些表达式在比较后返回布尔值,而不是互相排斥。
[..] why are there two separate definitions needed?
需要考虑的一件事是,实现这些运算符中的一个可能比使用另一个的求反更有效。
(我这里的例子是垃圾,但问题仍然存在,例如,想想布卢姆过滤器:如果某个东西不在一个集合中,它们允许快速测试,但是如果它在集合中,则测试可能需要更多的时间。)
[..] by definition, a != b is !(a == b).
这是你作为程序员的责任。编写测试可能是件好事。
- !((a == rhs.a) && (b == rhs.b))如何不允许短路?如果是!(a == rhs.a),则不评估(b == rhs.b)。
- 不过,这是一个不好的例子。短路在这里没有增加任何神奇的优势。
- @Oliver Charlesworth本身没有,但是当它与单独的运算符连接时,它确实这样做了:在==的情况下,一旦第一个对应元素不相等,它就会停止比较。但在!=的情况下,如果是按照==的方式实现的,则需要首先比较所有相应的元素(当它们都相等时),以便能够判断它们不相等:p但如上例所实施时,一旦找到第一个不相等的对,就会停止比较。的确是个很好的例子。
- @我的例子完全是胡说八道。不幸的是,我不能再拿出一台自动取款机了,这里太迟了。
- @barbarakwarc:!((a == b) && (c == d))和(a != b) || (c != d)在短路效率方面是等效的。
- 布卢姆过滤器的例子也很糟糕。集合成员身份与相等不同。我不认为有任何通过单独实现它们(对于运算符的标准含义)而获得性能的例子。因为一旦你找到某事物是否相等的答案,你就会知道它是否不相等。
- !=不可能比==更有效(假设a == b==!(a != b))。因为如果是这样的话,您可以用与!=相同的方式实现==,但输出是反向的。
是的,因为一个意思是"等价物",另一个意思是"不等价物",而且这个术语是互斥的。此运算符的任何其他含义都是混淆的,应尽量避免。
- 它们并非对所有情况都是互斥的。例如,两个无穷大既不相等又不相等。
- @在一般情况下,vladon可以使用一个代替另一个?不,这意味着他们不平等。其余的都是一个特殊的函数,而不是运算符=/!=
- @弗拉登,请阅读我答案中的所有案例,而不是一般案例。
- @Vladon在数学上也是这样,你能举一个例子吗?因为这个原因,在C语言中,a != b不等于!(a == b)?
通过自定义操作符的行为,可以让它们做您想要做的事情。
您可能希望定制物品。例如,您可能希望自定义一个类。只需检查特定属性,就可以比较此类的对象。既然知道了这一点,您就可以编写一些只检查最小值的特定代码,而不是检查整个对象中每个属性的每一位。
想象一下这样一个例子,在这个例子中,你可以发现有些东西和你发现的东西一样快,甚至不快。当然,一旦你发现某个事物是相同的还是不同的,那么你只需轻轻地翻转一下就可以知道相反的情况。然而,翻转该位是一个额外的操作。在某些情况下,当代码被重新执行很多次时,保存一个操作(乘以多次)可能会提高整体速度。(例如,如果您在百万像素屏幕上每像素保存一个操作,那么您刚刚保存了一百万个操作。乘以每秒60个屏幕,您可以保存更多操作。)
a href="http://stackoverflow.com/a/37800271/4411648">hvd的答案/aa提供了一些其他示例。
也许这是一个不可原谅的规则,其中a != b是假的,a == b是假的,就像一个无状态的比特。
1 2 3
| if( !(a == b || a != b) ){
// Stateless
} |
- 如果您想重新排列逻辑符号,那么!([A][B])逻辑变为([!A!B])
- 请注意,operator==()和operator!=()的返回类型不一定是bool,如果需要,它们可能是一个包含无状态的枚举,但是这些运算符可能仍然定义为(a != b) == !(a==b)持有。