== 和 != 相互依赖吗?

Are == and != mutually dependent?

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

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

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

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

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)

这是一个意见。也许不是。但是语言设计者并非无所不知,因此决定不限制可能想出可能有意义的情况的人(至少对他们而言)。

[..] why are there two separate definitions needed?

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

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

[..] by definition, a != b is !(a == b).

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

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

就平等而言,如果您的用例保证将对象视为不可比较的,那么每次比较都应该 return false(或者不可比较的结果类型,如果您的运算符return 非布尔值)。我想不出在什么特定情况下可以保证这样做,但我认为这是合理的。

a == b return 不是 时,您会 不希望语言自动将 a != b 重写为 !(a == b)一个bool。您可能会这样做的原因有几个。

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

的表达式节点

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

您可能有一些自定义 optional<T> 模板 class,其中给定可选变量 tu,您想允许 t == u,但是它 return 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== 的 "opposite"。

My concern is, though, why are there two separate definitions needed?

您不必同时定义两者。
如果它们互斥,您仍然可以通过仅在 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';
}

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?

我们经常将这些运算符与相等性联系起来。
尽管这是它们在基本类型上的行为方式,但没有义务要求它们在自定义数据类型上也是如此。 如果你不想,你甚至不必 return 布尔值。

我见过人们以奇怪的方式重载运算符,只是发现这对他们的特定领域应用程序有意义。即使界面上显示它们是互斥的,作者也可能想添加特定的内部逻辑。

(either from the user's perspective, or the implementer's perspective)

我知道你想要一个具体的例子,
所以这是 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) 是没有意义的。这样做的原因是框架可以打印出所做的比较。为此,它需要捕获使用了哪些重载运算符的上下文。

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;和原来的意思完全不同

因此,如果您接受运算符的含义在重载时发生变化,那么就没有理由阻止用户为运算符 == 赋予不完全是 [=21= 的含义]negation 运算符 !=,尽管这可能会造成混淆。

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

如果可能的话,创建这样的新示例可能不是一个好主意,因为它太不直观了,但是如果您要尝试对现有约定建模,那么可以选择让您的操作员表现得很好"correctly" 对于该上下文。

回应编辑;

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.

general中,不,没有意义。相等和关系运算符通常成套出现。有平等,就有不平等;小于,然后大于等等 <= 等。类似的方法也适用于算术运算符,它们通常也出现在自然逻辑集合中。

std::rel_ops 命名空间证明了这一点。如果您实现等于和小于运算符,则使用该命名空间可以为您提供其他的,根据您最初实现的运算符实现的。

综上所述,是否存在其中一个不会立即意味着另一个,或者无法根据其他实施的条件或情况?是的,有,可以说很少,但确实存在;同样,正如 rel_ops 是它自己的命名空间所证明的那样。出于这个原因,允许它们独立实现允许您利用语言以对代码的用户或客户来说仍然自然和直观的方式获得您需要或需要的语义。

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

可能是一个无法比较的规则,其中 a != bfalsea == bfalse 就像一个无状态位.

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

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

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-):

$ 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)。这些表达式 returns 是比较后的布尔值而不是互斥的。

通过自定义运算符的行为,您可以让它们为所欲为。

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

想象一下这样一种情况,您发现某些东西不同的速度与您发现某些东西相同的速度一样快(如果不是更快的话)。诚然,一旦你弄清楚了某些东西是相同的还是不同的,那么你只需稍微翻动一下就可以知道相反的东西。但是,翻转该位是一项额外的操作。在某些情况下,当代码被多次重新执行时,节省一次操作(乘以很多次)可以提高整体速度。 (例如,如果百万像素屏幕的每个像素节省一个操作,那么您就节省了一百万个操作。乘以每秒 60 个屏幕,您节省的操作甚至更多。)

提供了一些额外的示例。

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

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

是的,因为一个表示 "equivalent",另一个表示 "non-equivalent",并且这两个术语是互斥的。此运算符的任何其他含义都令人困惑,应无论如何避免。