296

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

我知道我可以根据另一个来定义一个,但这不是我要问的。我也不是在问按值或按身份比较对象之间的区别。或者两个对象是否可以同时相等和不相等(这绝对不是一个选择!这些东西是互斥的)。我要问的是:

在任何情况下,询问两个对象是否相等确实有意义,但询问它们相等是没有意义的?(无论是从用户的角度,还是从实施者的角度)

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

4

15 回答 15

274

希望语言自动重写a != b!(a == b)whena == b返回 a 以外的内容bool。您可能会这样做有几个原因。

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

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

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

可能还有更多我没有想到的。即使在这些示例中,操作a == ba != b执行都有意义,但仍然a != b与 不同!(a == b),因此需要单独的定义。

于 2016-06-13T22:41:13.057 回答
112

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

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

以 operator 为例,<<它最初是按位左移运算符,现在通常被重载为插入运算符,例如 in std::cout << something; 和原来的意思完全不同。

因此,如果您接受运算符的含义在重载时会发生变化,那么没有理由阻止用户赋予运算符==不完全是运算符否定!=的含义,尽管这可能会令人困惑。

于 2016-06-13T22:57:34.313 回答
61

不过,我担心的是,为什么需要两个单独的定义?

您不必同时定义两者。
如果它们是互斥的,您仍然可以通过仅定义==<旁边的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 测试框架:

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) 是没有意义的。这样做的原因是框架可以打印出所做的比较。为此,它需要捕获所使用的重载运算符的上下文。

于 2016-06-13T22:47:47.540 回答
45

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

如果可能的话,创建新的例子可能不是一个好主意,因为它太不直观了,但是如果你试图对现有的约定进行建模,那么最好选择让你的操作符“正确”地表现语境。

于 2016-06-14T05:54:10.783 回答
25

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

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

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

于 2016-06-14T23:11:00.430 回答
17

在任何情况下,询问两个对象是否相等确实有意义,但询问它们不相等是没有意义的?(无论是从用户的角度,还是从实施者的角度)

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

于 2016-06-13T22:30:22.967 回答
13

回应编辑;

也就是说,如果某些类型有可能有运算符==但没有!=,反之亦然,什么时候这样做才有意义。

一般来说不,这没有意义。等式和关系运算符通常成组出现。如果有平等,那么不平等也是如此;小于,然后大于等等,<=等等。类似的方法也适用于算术运算符,它们通常也出现在自然逻辑集合中。

这在std::rel_ops命名空间中得到了证明。如果您实现相等和小于运算符,则使用该命名空间会为您提供其他命名空间,这些命名空间是根据您最初实现的运算符实现的。

综上所述,有没有一种情况或情况不会立即意味着另一种,或者不能在其他方面实施?是的,可以说很少,但它们就在那里;再次,正如rel_ops它自己的命名空间所证明的那样。出于这个原因,允许它们独立实现允许您利用语言以对代码的用户或客户端仍然自然和直观的方式获得您需要或需要的语义。

已经提到的惰性评估就是一个很好的例子。另一个很好的例子是为它们提供根本不意味着相等或不相等的语义。与此类似的示例是位移运算符<<>>用于流插入和提取。尽管它在一般圈子中可能不受欢迎,但在某些特定领域的领域中它可能是有意义的。

于 2016-06-14T18:29:52.757 回答
12

如果==and!=运算符实际上并不意味着相等,就像<<and>>流运算符不意味着位移一样。如果您将符号视为表示其他概念,则它们不必相互排斥。

就相等而言,如果您的用例保证将对象视为不可比较的,那么每个比较都应返回 false(或不可比较的结果类型,如果您的运算符返回非布尔值),这可能是有意义的。我想不出有必要这样做的具体情况,但我认为这是足够合理的。

于 2016-06-13T22:39:16.533 回答
7

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

==并且!=可以超载做任何你想做的事。这既是祝福,也是诅咒。不能保证这!=意味着!(a==b).

于 2016-06-18T16:12:27.647 回答
6
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

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

我无法证明这种运算符重载是合理的,但在上面的示例中,不可能将其定义operator!=operator==.

于 2016-06-13T22:44:27.063 回答
5

最后,您使用这些运算符检查的是表达式a == bora != b正在返回一个布尔值(truefalse)。这些表达式在比较后返回一个布尔值,而不是互斥。

于 2016-06-15T04:58:09.810 回答
4

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

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

(我这里的例子很垃圾,但重点仍然存在,想想布隆过滤器,例如:如果某物不在集合中,它们允许快速测试,但测试它是否在可能需要更多时间。)

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

作为程序员,你有责任做到这一点。写一个测试可能是件好事。

于 2016-06-13T22:31:40.400 回答
2

通过自定义操作员的行为,您可以让他们为所欲为。

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

想象一下这样一种情况,您可以以同样快的速度(如果不快的话)找出不同的东西,而不是更快地找出相同的东西。当然,一旦你弄清楚某件事是相同的还是不同的,那么你只需稍微翻转一下就可以知道相反的情况。但是,翻转该位是一项额外的操作。在某些情况下,当代码被大量重新执行时,保存一个操作(乘以很多倍)可以提高整体速度。(例如,如果您为百万像素屏幕的每个像素保存一个操作,那么您刚刚保存了一百万个操作。乘以每秒 60 个屏幕,您可以保存更多操作。)

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

于 2016-06-18T00:20:42.850 回答
2

也许是一个无可比拟的规则,哪里a != ba == b的,哪里是的,就像一个无状态的位。

if( !(a == b || a != b) ){
    // Stateless
}
于 2016-06-14T19:09:52.680 回答
2

是的,因为一个表示“等效”,另一个表示“非等效”,并且这些术语是互斥的。此运算符的任何其他含义都令人困惑,应尽量避免。

于 2016-06-29T23:01:48.627 回答