相等运算符重载:是 (x!=y) == (!(x==y))?

Equality operator overloads: Is (x!=y) == (!(x==y))?

C++ 标准是否保证 (x!=y) 总是与 !(x==y) 具有相同的真值?


我知道这里涉及 许多 微妙之处:运算符 ==!= 可能被重载。它们可能被重载以具有不同的 return 类型(只需要隐式转换为 bool)。甚至 ! 运算符也可能在 return 类型上重载。这就是为什么我手忙脚乱地提到了上面的“真值”,但试图进一步阐述它,利用到 bool 的隐式转换,并试图消除可能的歧义:

bool ne = (x!=y);
bool e = (x==y);
bool result = (ne == (!e));

这里result保证是true吗?

C++ 标准在第 5.10 节中指定了相等运算符,但似乎主要是在语法上定义它们(以及一些关于指针比较的语义)。 存在EqualityComparable的概念,但没有专门说明其运算符==!=运算符的关系。

存在related documents from C++ working groups,说...

It is vital that equal/unequal [...] behave as boolean negations of each other. After all, the world would make no sense if both operator==() and operator!=() returned false! As such, it is common to implement these operators in terms of each other

然而,这仅反映 Common Sense™,并没有指定它们必须像这样实施。


一些背景知识:我只是想编写一个函数来检查两个值(未知类型)是否相等,如果不相等则打印一条错误消息。我想说这里需要的概念是类型是EqualityComparable。但是为此,人们仍然必须写 if (!(x==y)) {…} 并且可以 而不是 if (x!=y) {…},因为这将使用不同的运算符,这不包含在概念中EqualityComparable,甚至可能以不同的方式重载...


我知道程序员基本上可以在他的自定义重载中为所欲为。我只是想知道他是否真的允许做任何事情,或者是否有标准规定的规则。也许这些微妙的陈述之一表明偏离通常的实现会导致未定义的行为,例如 。例如,标准 明确地 声明对于 容器类型 a!=b 等同于 !(a==b)(第 23.2.1 节, table 95, "容器要求").

但是对于一般的、自定义的类型,目前好像没有这样的要求。这个问题被标记为 language-lawyer,因为我希望得到一个明确的 statement/reference,但我知道这几乎是不可能的:虽然有人可以指出其中表示运算符 的部分 是彼此的否定,很难证明标准的 ~1500 页中的 none 是这样说的...

有疑问,除非有进一步的提示,否则我稍后会 upvote/accept 相应的答案,现在假设比较 EqualityComparable 类型的不相等应该用 if (!(x==y)) 为了安全起见。

Does the C++ standard guarantee that (x!=y) always has the same truth value as !(x==y)?

不,不是。绝对没有什么能阻止我写作:

struct Broken {
    bool operator==(const Broken& ) const { return true; }
    bool operator!=(const Broken& ) const { return true; }
};

Broken x, y;

这是格式正确的代码。从语义上讲,它已损坏(顾名思义),但从纯 C++ 代码功能的角度来看,它肯定没有错。

标准在[over.oper]/7中也明确指出这是可以的:

The identities among certain predefined operators applied to basic types (for example, ++a ≡ a+=1) need not hold for operator functions. Some predefined operators, such as +=, require an operand to be an lvalue when applied to basic types; this is not required by operator functions.

同样,C++ 标准中没有任何内容保证 operator< 实际上实现了有效的排序(或 x<y <==> !(x>=y),等等)。一些标准库实现实际上会添加工具以尝试在有序容器中为您调试它,但这只是一个实现质量问题,而不是一个基于标准兼容的决定。


存在像 Boost.Operators 这样的库解决方案,至少可以使程序员这方面更容易一些:

struct Fixed : equality_comparable<Fixed> {
    bool operator==(const Fixed&) const;
    // a consistent operator!= is provided for you
};

在 C++14 中,Fixed 不再是基数 class 的聚合。但是,在 C++17 中,它又是一个聚合(通过 P0017)。


随着 C++20 采用 P1185,库解决方案实际上已成为一种语言解决方案 - 您只需编写以下内容:

struct Fixed {
    bool operator==(Fixed const&) const;
};

bool ne(Fixed const& x, Fixed const& y) {
    return x != y;
}

ne() 的主体成为一个有效的表达式,计算结果为 !x.operator==(y)——因此您不必担心保持两个比较一致,也不必依赖库解决方案来提供帮助出去。

没有。您可以为 ==!= 编写运算符重载来执行您想要的任何操作。这样做可能不是一个好主意,但 C++ 的定义并没有将这些运算符限制为彼此的逻辑对立。

总的来说,我不认为你可以依赖它,因为它并不总是对 operator ==operator!=always[=29= 有意义] 对应,所以我不明白标准怎么会要求它。

例如, 考虑内置浮点类型,如双精度数,NaNs 总是比较 false,因此 operator= = 和 operator!= 可以同时 return false。 (编辑:糟糕,这是错误的;请参阅 hvd 的评论。)

因此,如果我正在编写一个具有浮点语义的新 class(可能是 really_long_double),我 来实现相同的行为与原始类型一致,所以我的 operator== 必须表现相同并将两个 NaN 比较为 false,即使 operator!= 也将它们比较为 false。

在其他情况下也可能会出现这种情况。例如,如果我正在编写一个 class 来表示一个数据库可为 null 的值,我可能会 运行 进入同一个问题,因为所有与数据库 NULL 的比较都是错误的。我可能会选择在我的 C++ 代码中实现该逻辑以具有与数据库相同的语义。

但实际上,对于您的用例,可能不值得担心这些边缘情况。只需记录您的函数使用 operator== (or operator !=) 比较对象并保留它。