提问人:BarbaraKwarc 提问时间:6/14/2016 最后编辑:Baum mit AugenBarbaraKwarc 更新时间:9/7/2023 访问量:24262
== 和 != 是相互依赖的吗?
Are == and != mutually dependent?
问:
我正在学习 C++ 中的运算符重载,我看到它只是一些可以为用户定义类型自定义的特殊函数。但是,我担心的是,为什么需要两个单独的定义?我认为如果是真的,那么它会自动为假,反之亦然,没有其他可能性,因为根据定义,是.我无法想象任何情况都不是真的。但也许我的想象力有限,或者我对某些事情一无所知?==
!=
a == b
a != b
a != b
!(a == b)
我知道我可以用另一个来定义一个,但这不是我要问的。我也不是在问按价值或按身份比较对象之间的区别。或者两个对象是否可以同时相等和不相等(这绝对不是一个选项!这些东西是相互排斥的)。我要问的是:
有没有一种情况,问两个物体相等的问题确实有意义,但问它们不相等就没有意义了?(从用户的角度,或从实现者的角度)
如果没有这种可能性,那么为什么 C++ 将这两个运算符定义为两个不同的函数呢?
答:
是否有任何情况可以提出关于两个的问题 对象相等确实是有道理的,但问它们不是 平等没有意义?(无论是从用户的角度来看,还是 实施者的观点)
这是一种意见。也许不是。但是语言设计者并不是无所不知的,他们决定不限制那些可能想出有意义的情境的人(至少对他们来说是这样)。
[..]为什么需要两个单独的定义?
需要考虑的一件事是,有可能更有效地实现其中一个运算符,而不仅仅是使用另一个运算符的否定。
(我这里的例子是垃圾,但重点仍然成立,例如,想想布隆滤镜:如果某些东西不在集合中,它们允许快速测试,但测试它是否在集合中可能需要更多的时间。
[..]根据定义,是 .
a != b
!(a == b)
作为程序员,你有责任做到这一点。写测试可能是一件好事。
评论
!((a == rhs.a) && (b == rhs.b))
!(a == rhs.a)
(b == rhs.b)
==
!=
==
!((a == b) && (c == d))
(a != b) || (c != d)
如果 and 运算符实际上并不意味着相等,就像 and 流运算符不意味着位移一样。如果将符号视为其他概念,则它们不必相互排斥。==
!=
<<
>>
在相等性方面,如果您的用例需要将对象视为不可比,那么这可能很有意义,以便每次比较都应返回 false(或者如果您的运算符返回非 bool,则返回不可比的结果类型)。我想不出有什么具体情况可以保证这样做,但我可以看到它足够合理。
您不希望语言自动重写,因为 when 返回 .有几个原因可以让你这样做。a != b
!(a == b)
a == b
bool
您可能有表达式生成器对象,其中不执行任何比较,也不打算执行任何比较,而只是构建一些表示 .a == b
a == b
你可能有惰性评估,其中没有也不打算直接执行任何比较,而是返回某种可以在以后的某个时间隐式或显式转换为实际比较的评估。可能与表达式生成器对象结合使用,以便在计算之前进行完整的表达式优化。a == b
lazy<bool>
bool
您可能有一些自定义模板类,其中给定了可选变量和 ,您希望允许 ,但使其返回 。optional<T>
t
u
t == u
optional<bool>
可能还有更多我没有想到的。尽管在这些示例中,操作和 do 都有意义,但仍然与 不是一回事,因此需要单独的定义。a == b
a != b
a != b
!(a == b)
评论
!=
==
!
!
a != b
!(a == b)
lazy<bool>::operator!
lazy<bool>
optional<bool>
boost::optional
Nan
NaN
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';
}
是否有任何情况可以提出关于两个的问题 对象相等确实是有道理的,但问它们不是 平等没有意义?
我们经常将这些运算符与平等联系在一起。
尽管这是它们在基本类型上的行为方式,但没有义务这是它们在自定义数据类型上的行为。
如果您不想,您甚至不必返回布尔值。
我见过人们以奇怪的方式使运算符过载,结果发现这对他们特定于领域的应用程序是有意义的。即使接口显示它们是互斥的,作者也可能希望添加特定的内部逻辑。
(从用户的角度,或从实现者的角度)
我知道你想要一个具体的例子,
所以这里有一个我认为很实用的 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 );
}
这些运算符正在做不同的事情,将一种方法定义为 !(不是)另一个。这样做的原因是为了让框架可以打印出所做的比较。为此,它需要捕获使用了哪些重载运算符的上下文。
评论
std::rel_ops
rel_ops
如果没有这种可能性,那么为什么 C++ 将这两个运算符定义为两个不同的函数呢?
因为你可以重载它们,通过重载它们,你可以赋予它们与原始含义完全不同的含义。
以运算符为例,最初是按位左移运算符,现在通常作为插入运算符重载,如 in ;与原来的意思完全不同。<<
std::cout << something
因此,如果您接受运算符的含义在重载时会发生变化,那么就没有理由阻止用户给运算符赋予不完全是运算符否定的含义,尽管这可能会令人困惑。==
!=
评论
==
!=
有一些非常成熟的约定,其中 和 都是错误的,不一定是对立的。特别是,在 SQL 中,任何与 NULL 的比较都会产生 NULL,而不是 true 或 false。(a == b)
(a != b)
如果可能的话,创建新的示例可能不是一个好主意,因为它太不直观了,但是如果您尝试对现有约定进行建模,那么可以选择让您的运算符在该上下文中“正确”运行是件好事。
评论
NULL == something
NULL != something
!Unknown
Unknown
operator!=
operator==
operator==
operator!=
operator!=
operator==
X == null
X != null
null
false
null
false
not (X == null)
not (X != null)
true
作为对编辑的回应;
也就是说,如果某种类型可能具有运算符,但没有运算符,反之亦然,以及何时这样做是有意义的。
==
!=
一般来说,不,这没有意义。相等运算符和关系运算符通常以集合形式出现。如果存在平等,那么不平等也存在;小于,然后大于,依此类推。类似的方法也适用于算术运算符,它们通常也出现在自然逻辑集中。<=
std::rel_ops
命名空间证明了这一点。如果实现相等和小于运算符,则使用该命名空间将为您提供其他运算符,这些运算符是根据原始实现的运算符实现的。
总而言之,是否存在一个条件或情况,其中一个不会立即意味着另一个,或者不能根据其他条件实施?是的,可以说很少,但它们就在那里;同样,正如它自己的命名空间所证明的那样。出于这个原因,允许它们独立实现,可以让你利用语言来获取你需要或需要的语义,而这种语义对于代码的用户或客户端来说仍然是自然和直观的。rel_ops
前面提到的懒惰评估就是一个很好的例子。另一个很好的例子是给他们提供根本不意味着平等或不平等的语义。与此类似的示例是位移运算符,用于流插入和提取。虽然它可能在一般圈子里不受欢迎,但在某些特定领域,它可能是有道理的。<<
>>
也许是一个无与伦比的规则,哪里是错误的,并且像无状态位一样是错误的。a != b
a == b
if( !(a == b || a != b) ){
// Stateless
}
评论
operator==()
operator!=()
bool
(a != b) == !(a==b)
我只会回答你问题的第二部分,即:
如果没有这种可能性,那么为什么 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'
这可能被认为是微优化,但在某些情况下可能是必要的。
评论
pcmpeqb
指令,但没有生成 != 掩码的打包比较指令。因此,如果你不能反转任何使用结果的逻辑,你必须使用另一条指令来反转它。(有趣的事实:AMD 的 XOP 指令集确实有 packed-compare。太糟糕了,英特尔没有采用/扩展 XOP;在那个即将消亡的 ISA 扩展中有一些有用的说明。neq
PXOR
x == y
x != y
最后,您使用这些运算符检查的是表达式 or 返回布尔值 ( 或 )。这些表达式在比较后返回布尔值,而不是互斥。a == b
a != b
true
false
通过自定义操作员的行为,您可以让他们做你想做的事。
您可能希望自定义内容。例如,您可能希望自定义一个类。只需检查特定属性即可比较此类的对象。知道是这种情况,您可以编写一些特定的代码来只检查最少的东西,而不是检查整个对象中每个属性的每一位。
想象一下,你可以发现某些东西是不同的,即使不是更快,也比你发现东西是一样的一样快。当然,一旦你弄清楚某件事是相同还是不同,那么你只需翻转一下就可以知道相反的情况。但是,翻转该位是一项额外的操作。在某些情况下,当代码被大量重新执行时,保存一个操作(乘以许多倍)可以提高整体速度。(例如,如果百万像素屏幕的每个像素保存一个操作,那么您刚刚保存了一百万个操作。乘以每秒 60 个屏幕,您可以节省更多操作。
HVD的回答提供了一些额外的例子。
强大的力量会带来巨大的责任感,或者至少是非常好的风格指南。
==
并且可以超载以做任何您想做的事情。这既是祝福也是诅咒。不能保证这意味着.!=
!=
!(a==b)
是的,因为一个表示“等效”,另一个表示“非等同”,并且这些术语是相互排斥的。此运算符的任何其他含义都令人困惑,应尽可能避免。
评论
a != b
!(a == b)
评论
'undefined' != expression
a!=b
!(a==b)
b
b
(NaN != NaN) == true