等式运算符重载:Is (x!=y) == (!(x==y))?

Posted

技术标签:

【中文标题】等式运算符重载:Is (x!=y) == (!(x==y))?【英文标题】:Equality operator overloads: Is (x!=y) == (!(x==y))? 【发布时间】:2016-06-26 21:28:38 【问题描述】:

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


我知道这里涉及许多微妙之处:运算符==!= 可能过载。它们可能被重载以具有不同的返回类型(只需隐式转换为bool)。甚至 !-operator 也可能在返回类型上被重载。这就是为什么我在上面提到了“真值”,但试图进一步阐述它,利用隐式转换为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,说的是……

至关重要的是,相等/不等 [...] 表现为彼此的布尔否定。毕竟,如果 operator==() 和 operator!=() 都返回 false,世界将毫无意义!因此,通常根据彼此来实现这些运算符

但是,这仅反映了 Common Sense™,并没有指定它们必须像这样实现。


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


我知道程序员基本上可以在他的自定义重载中为所欲为。我只是想知道他是否真的允许做所有事情,或者是否有标准强加的规则。也许这些微妙的陈述之一表明偏离通常的实现会导致未定义的行为,比如NathanOliver mentioned in a comment, but which seemed to only refer to certain types。例如,标准明确声明对于容器类型a!=b 等同于!(a==b)(第 23.2.1 节,表 95,“容器要求”)。

但是对于一般的、用户定义的类型,目前似乎没有这样的要求。这个问题被标记为language-lawyer,因为我希望有一个明确的声明/参考,但我知道这几乎是不可能的:虽然有人可以指出它说运营商必须的部分相互否定,很难证明该标准的约 1500 页中没有一个是这样说的……

有疑问,除非有进一步的提示,否则我稍后会支持/接受相应的答案,现在假设比较 EqualityComparable 类型的不相等性应该使用 if (!(x==y)) 来完成安全的一面。

【问题讨论】:

我猜你只关心内置类型,因为用户定义的类型可以做任何他们想做的事情。 自定义类型可能对两者都有不同的值(由于重载了这些运算符),但它是不好的样式。 TL;DR -- operator!=operator== 是留给开发人员实现的两个不同功能,没有什么可以保证对称性 顺便说一句,您可以认为这类似于许多标准接口非常谨慎地仅使用 < 的方式,以便不要求存在 <=>>= 和是一致的。 @DavidSchwartz 我明白了,您的意思可能类似于NaN,现在答案中也提到了。 (附注:这意味着 STL 实现可能基本上从不使用!= 运算符根本,因为它没有被任何概念所涵盖——也许我'将挖掘一些 STL 源,看看是否真的是这样......) 【参考方案1】:

C++ 标准是否保证(x!=y) 始终与!(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 可以这样做:

应用于基本类型的某些预定义运算符之间的标识(例如,++a ≡ a+=1)不需要为运算符函数保留。一些预定义的运算符,例如+=,在应用于基本类型时要求操作数是左值;这不是操作员函数所必需的。

同样,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 不再是基类的聚合。但是,在 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)——因此您不必担心保持两个比较一致,也不必依赖库解决方案来提供帮助。

【讨论】:

如果两个操作数比较相等,则 == 运算符的结果为真,!= 运算符的结果为假。如果两个操作数比较不相等,则 == 运算符的结果为 false,!= 运算符的结果为 true。否则,每个运算符的结果都是未指定的。 from [expr.eq]? @NathanOliver 该部分仅与那些满足“操作数应具有算术、枚举、指针或指向成员类型的指针,或类型std::nullptr_t。”的类型有关。 @NathanOliver 这指的是哪个版本的标准?在 3337 中,这似乎没有被包含(我认为这只是“实际”标准的 扩展 版本(成本为 $$$,所以我这里没有) ) @Marco13 那太老了。最新的工作草案是N4567 @Marco13 我的文字来自 N3797。【参考方案2】:

没有。您可以为 ==!= 编写运算符重载,它们可以随心所欲。这样做可能是个坏主意,但 C++ 的定义并没有将这些运算符限制为彼此的逻辑对立。

【讨论】:

【参考方案3】:

总的来说,我认为你不能依赖它,因为operator ==operator!= 并不总是有意义总是 对应,所以我看不到标准如何要求它。

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

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

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

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

【讨论】:

与数据库NULL 的比较过于简单化了一些。大多数比较结果为NULL,但也有一些例外。 1 = 1 OR 1 = null 将为真,因为它简化为true OR null,并且在正常的逻辑规则中为真或任何东西都是真的。在确定数据库将如何处理它们时,考虑NULL 的最佳方式是将它们视为“未知”。当然,这并不会使您的观点无效,但这是一个需要注意的重要区别。 "考虑内置的浮点类型,比如双精度数,NaN 总是比较为假" -- != 除外。 @hvd 我怎么能忽略这个?这很尴尬。即使对于 NaN,它们仍然是彼此的否定。我暂时不接受这个,也许有人提出了一个令人信服的观点,说明为什么他们(真的)不能互相否定...... @jpmc26 从技术上讲,true OR NULL 不是与 NULL 的比较(即它是逻辑运算而不是比较运算),这就是结果不是 NULL 的原因;不过,谢谢你的澄清 @hvd Errr...哎呀。感谢指正;我已经编辑了我的答案以表明我错了。

以上是关于等式运算符重载:Is (x!=y) == (!(x==y))?的主要内容,如果未能解决你的问题,请参考以下文章

运算符重载 c++ (x==y==z)

为啥我们必须重载“<”运算符才能使用 is_permutation 并包含算法

Python 运算符重载

TensorFlow 运算符重载

运算符重载

Swift语言精要 - Operator(运算符重载)