我正在学习 C++ 中的运算符重载,我看到 ==
和 !=
只是一些可以为用户定义的类型定制的特殊函数。不过,我担心的是,为什么需要两个单独的定义?我认为如果a == b
为真,那么a != b
自动为假,反之亦然,没有其他可能,因为根据定义,a != b
就是!(a == b)
。我无法想象在任何情况下这是不正确的。但也许我的想象力有限,或者我对某些事情一无所知?
我知道我可以根据另一个来定义一个,但这不是我要问的。我也不是在问按值或按身份比较对象之间的区别。或者两个对象是否可以同时相等和不相等(这绝对不是一个选择!这些东西是互斥的)。我要问的是:
在任何情况下,询问两个对象是否相等确实有意义,但询问它们不相等是没有意义的? (无论是从用户的角度,还是从实施者的角度)
如果没有这种可能性,那么究竟为什么 C++ 将这两个运算符定义为两个不同的函数?
'undefined' != expression
始终为真(或假,或未定义),无论是否可以计算表达式。在这种情况下,a!=b
将根据定义返回正确的结果,但如果无法评估 b
,!(a==b)
将失败。 (或者如果评估 b
代价高昂,则需要花费大量时间)。
(NaN != NaN) == true
的示例
当 a == b
返回的不是 bool
时,您不希望语言自动将 a != b
重写为 !(a == b)
。您可能会这样做有几个原因。
您可能有表达式构建器对象,其中 a == b
不执行也不打算执行任何比较,而只是构建一些表示 a == b
的表达式节点。
您可能有惰性求值,其中 a == b
不会也不打算直接执行任何比较,而是返回某种可以在以后隐式或显式转换为 bool
的 lazy<bool>
进行比较。可能与表达式构建器对象结合使用,以允许在评估之前完成表达式优化。
您可能有一些自定义 optional<T>
模板类,其中给定可选变量 t
和 u
,您希望允许 t == u
,但使其返回 optional<bool>
。
可能还有更多我没有想到的。即使在这些示例中,操作 a == b
和 a != b
都有意义,但 a != b
仍然与 !(a == b)
不同,因此需要单独定义。
如果没有这种可能性,那么究竟为什么 C++ 将这两个运算符定义为两个不同的函数?
因为你可以重载它们,并且通过重载它们,你可以赋予它们与原来的含义完全不同的含义。
以运算符 <<
为例,它最初是按位左移运算符,现在通常被重载为插入运算符,如 std::cout << something
;和原来的意思完全不同。
因此,如果您接受重载时运算符的含义会发生变化,那么没有理由阻止用户赋予运算符 ==
不完全是运算符 { 的 否定 的含义2},尽管这可能会令人困惑。
==
和 !=
作为不同的运算符存在。另一方面,它们可能不作为不同的运算符存在,因为您可以单独重载它们,但由于遗留和方便(代码简洁)的原因。
不过,我担心的是,为什么需要两个单独的定义?
您不必同时定义两者。
如果它们是互斥的,您仍然可以通过在 std::rel_ops 旁边仅定义 ==
和 <
来保持简洁
形式 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 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 );
}
这些运算符在做不同的事情,将一种方法定义为另一种方法的 !(not) 是没有意义的。这样做的原因是框架可以打印出所做的比较。为此,它需要捕获所使用的重载运算符的上下文。
std::rel_ops
?非常感谢您指出这一点。
rel_ops
无论如何都是可怕的。
有一些非常成熟的约定,其中 (a == b)
和 (a != b)
都是 都是错误的 不一定是相反的。特别是,在 SQL 中,任何与 NULL 的比较都会产生 NULL,而不是 true 或 false。
如果可能的话,创建新的例子可能不是一个好主意,因为它太不直观了,但是如果你试图对现有的约定进行建模,那么最好选择让你的操作符“正确”地表现语境。
NULL == something
返回 Unknown,并且您还希望 NULL != something
返回 Unknown,并且您希望 !Unknown
返回 Unknown
。在那种情况下,将 operator!=
实现为 operator==
的否定仍然是正确的。
operator==
或 operator!=
之一,但不实现另一个,或 2) 以不同于 operator==
否定的方式实现 operator!=
.为 NULL 值实现 SQL 逻辑并非如此。
X == null
和 X != null
肯定会评估为 null
,而不是 false
。我怎么知道,你可能会问? a) 这些值显示为 null
,而不是 false
b) not (X == null)
和 not (X != null)
不计算为 true
,这是每个 sql 程序员在某个时候学到的一课。确实,我相信所有主要的sql 实现非常严格地遵守(一些迭代)sql 标准。
我只会回答你问题的第二部分,即:
如果没有这种可能性,那么究竟为什么 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 指令集确实对 neq
进行了打包比较。太糟糕的是英特尔没有采用/扩展 XOP;在即将失效的 ISA 扩展中有一些有用的指令。)
PXOR
与全一以反转比较掩码结果)可能很重要。
x == y
的成本远高于 x != y
,则可能不止一个逻辑否定。由于分支预测等原因,计算后者可能要便宜得多。
在任何情况下,询问两个对象是否相等确实有意义,但询问它们不相等是没有意义的? (无论是从用户的角度,还是从实施者的角度)
这是一种意见。也许不是。但是语言设计者并不是无所不知的,他们决定不限制那些可能提出可能有意义的情况的人(至少对他们来说)。
回应编辑;
也就是说,如果某些类型有可能有运算符 == 而不是 !=,反之亦然,什么时候这样做才有意义。
在一般中,不,这没有意义。等式和关系运算符通常成组出现。如果有平等,那么不平等也是如此;小于,然后大于,以此类推 <=
等。类似的方法也适用于算术运算符,它们通常也出现在自然逻辑集合中。
这在 std::rel_ops
命名空间中得到了证明。如果您实现相等和小于运算符,则使用该命名空间会为您提供其他命名空间,这些命名空间是根据您最初实现的运算符实现的。
综上所述,是否存在一种情况或情况,其中一个不会立即意味着另一个,或者不能在其他方面实施?是的,有,可以说很少,但它们就在那里;同样,正如 rel_ops
是它自己的命名空间所证明的那样。出于这个原因,允许它们独立实现允许您利用语言以对代码的用户或客户端仍然自然和直观的方式获得您需要或需要的语义。
已经提到的惰性评估就是一个很好的例子。另一个很好的例子是为它们提供根本不意味着相等或不相等的语义。与此类似的示例是用于流插入和提取的位移运算符 <<
和 >>
。尽管它在一般圈子中可能不受欢迎,但在某些特定领域的领域中它可能是有意义的。
如果 ==
和 !=
运算符实际上并不意味着相等,则与 <<
和 >>
流运算符不意味着位移一样。如果您将符号视为表示其他概念,则它们不必相互排斥。
就相等而言,如果您的用例保证将对象视为不可比较的,那么每个比较都应返回 false(或不可比较的结果类型,如果您的运算符返回非布尔值),这可能是有意义的。我想不出有必要这样做的具体情况,但我认为这是足够合理的。
强大的力量带来了巨大的责任感,或者至少是非常好的风格指南。
==
和 !=
可以重载以做任何你想做的事。这既是祝福,也是诅咒。不能保证 !=
表示 !(a==b)
。
enum BoolPlus {
kFalse = 0,
kTrue = 1,
kFileNotFound = -1
}
BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);
我不能证明这个运算符重载是合理的,但在上面的示例中,不可能将 operator!=
定义为 operator==
的“对立面”。
!=
确实不意味着 ==
的反义词。
最后,您使用这些运算符检查的是表达式 a == b
或 a != b
返回一个布尔值(true
或 false
)。这些表达式在比较后返回一个布尔值,而不是互斥。
[..] 为什么需要两个单独的定义?
需要考虑的一件事是,可能比仅使用另一个运算符的否定更有效地实现其中一个运算符。
(我这里的例子很垃圾,但重点仍然存在,想想布隆过滤器,例如:如果某物不在集合中,它们允许快速测试,但测试它是否在可能需要更多时间。)
[..] 根据定义,a != b 是 !(a == b)。
作为程序员,你有责任做到这一点。写一个测试可能是件好事。
!((a == rhs.a) && (b == rhs.b))
如何不允许短路?如果是 !(a == rhs.a)
,则不会评估 (b == rhs.b)
。
==
的情况下,一旦第一个对应元素不相等,它将停止比较。但是在 !=
的情况下,如果它是根据 ==
实现的,则需要首先比较所有相应的元素(当它们都相等时)才能判断它们不是不相等的: P 但是当按照上面的例子实现时,它会在找到第一个不相等的对时立即停止比较。确实很好的例子。
!((a == b) && (c == d))
和 (a != b) || (c != d)
在短路效率方面是等效的。
通过自定义操作员的行为,您可以让他们为所欲为。
您可能希望自定义内容。例如,您可能希望自定义一个类。只需检查特定属性即可比较此类的对象。知道是这种情况,您可以编写一些仅检查最少事物的特定代码,而不是检查整个对象中每个属性的每个位。
想象一下这样一种情况,您可以以同样快的速度(如果不快的话)找出不同的东西,而不是更快地找出相同的东西。当然,一旦你弄清楚某件事是相同的还是不同的,那么你只需稍微翻转一下就可以知道相反的情况。但是,翻转该位是一项额外的操作。在某些情况下,当代码被大量重新执行时,保存一个操作(乘以很多倍)可以提高整体速度。 (例如,如果您为百万像素屏幕的每个像素保存一个操作,那么您刚刚保存了一百万个操作。乘以每秒 60 个屏幕,您可以保存更多操作。)
hvd's answer 提供了一些其他示例。
是的,因为一个表示“等效”,另一个表示“非等效”,并且这些术语是互斥的。此运算符的任何其他含义都令人困惑,应尽量避免。
a != b
不等于 !(a == b)
吗?
也许是一个无法比较的规则,其中 a != b
是 false 而 a == b
是 false 就像一个无状态位。
if( !(a == b || a != b) ){
// Stateless
}
operator==()
和 operator!=()
的返回类型不一定是 bool
,如果您愿意,它们可能是包含无状态的枚举,但仍可能定义运算符,因此 (a != b) == !(a==b)
成立。
!=
而不是两次通过计算==
然后!
。尤其是在你不能依赖编译器来融合循环的那一天。甚至在今天,如果您无法说服编译器您的向量不会重叠。!
也可以构建一些表达式节点,我们仍然可以将a != b
替换为!(a == b)
,就目前而言。lazy<bool>::operator!
也是如此,它可以返回lazy<bool>
。optional<bool>
更有说服力,因为例如boost::optional
的逻辑真实性取决于值是否存在,而不取决于值本身。Nan
- 请记住NaN
;