为什么在许多C ++标准库代码中将不等式测试为(!(a == b))?


142

这是C ++标准库remove代码中的代码。为什么用不平等if (!(*first == val))来代替if (*first != val)

 template <class ForwardIterator, class T>
      ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val)
 {
     ForwardIterator result = first;
     while (first!=last) {
         if (!(*first == val)) {
             *result = *first;
             ++result;
         }
         ++first;
     }
     return result;
 }

2
@BeyelerStudios可能是正确的。在实现时也很常见operator!=。只需使用operator==实现:bool operator!=(const Foo& other) { return !(*this == other); }
simon 2015年

1
实际上,我纠正了我的说法:参考文献中提及的内容删除了所有等于值的元素因此operator==有望在此处使用...
BeyelerStudios

哦,const在我之前的评论中的示例中也应该有一个,但是您明白了。(为时已晚,无法对其进行编辑)
simon 2015年

这样做的原因与另一个问题(基本上可以用“否,不一定”回答)有关,也涉及EqualityComparable到Hurkyl在回答中提到的概念。
Marco13 '18

Answers:


144

因为这意味着对T的唯一要求是实现一个operator==。您可能需要T具有一个,operator!=但是这里的总体思路是,您应该尽可能减少模板用户的负担,而其他模板确实需要operator==


13
模板<class T>内联布尔运算符!= <T a,T b> {return!(a == b); }
约书亚

8
在任何情况下,编译器将无法交换=的所有实例!到!(==)?为什么这还不是编译器要采取的默认操作?
艾丹·戈麦斯

20
@AidanGomez不管是好是坏,您都可以使运算符重载以执行您想要的任何事情。它不一定有意义,也不必保持一致。
尼尔·柯克

10
x != y未定义为与相同!(x == y)。如果这些运算符返回嵌入式DSL的解析树怎么办?
布里斯·登普西

7
@Joshua如果尝试使用SFINAE来检测是否!=受支持,则会严重中断(即使不支持,也会错误地返回true operator==!)。我还担心它会导致某些用途!=变得模棱两可。

36

STL中的大多数功能只能与operator<或一起使用operator==。这就要求用户仅实现这两个运算符(或有时至少其中之一)。例如std::set使用operator<(更确切地说std::lessoperator<默认情况下调用哪个)而不operator>管理订单。remove您的示例中的模板是类似的情况-它仅使用operator==而不使用,operator!=因此operator!=不需要定义。


2
函数不operator<直接使用std::less,而是使用,后者默认为operator<
Christian Hackl

1
实际上,似乎标准算法功能std::set确实不同于,例如,确实可以operator<直接使用。奇怪...
Christian Hackl 2015年

1
这些函数不使用std::equal_tooperator==而是按问题中的说明使用。与情况std::less类似。好吧,也许std::set不是最好的例子。
卢卡斯·贝德纳里克

2
@ChristianHackl,std::equal_tostd::less用作默认模板参数,其中将比较器用作参数。operator==并且operator<直接用于需要类型分别满足相等的可比较和严格弱排序的地方,例如迭代器和随机访问迭代器。
Jan Hudec

28

这是来自C ++标准库的删除代码。

错误。这不是 C ++标准库remove的代码。这是C ++标准库函数的一种可能的内部实现remove。C ++标准没有规定实际的代码。它规定了函数原型和必需的行为。

换句话说:从严格的语言角度来看,您所看到的代码不存在。它可能来自编译器的标准库实现随附的某些头文件。请注意,C ++标准甚至不需要这些头文件存在。文件只是编译器实现者满足类似代码行#include <algorithm>(即std::remove提供可用功能和其他功能)的一种便捷方式。

为什么用不平等if (!(*first == val))来代替if (*first != val)

因为仅operator==此功能需要。

对于自定义类型的运算符重载,该语言允许您执行各种奇怪的事情。您可以很好地创建一个具有重载operator==但没有重载的类operator!=。甚至更糟糕的是:您可能会超载,operator!=但会做完全不相关的事情。

考虑以下示例:

#include <algorithm>
#include <vector>

struct Example
{
    int i;

    Example() : i(0) {}

    bool operator==(Example const& other) const
    {
        return i == other.i;
    }

    bool operator!=(Example const& other) const
    {
        return i == 5; // weird, but nothing stops you
                       // from doing so
    }

};

int main()
{
  std::vector<Example> v(10);
  // ...
  auto it = std::remove(v.begin(), v.end(), Example());
  // ...
}

如果std::remove使用operator!=,则结果将完全不同。


1
另一个要考虑的是,它可能两者是可能的a==b,并a!=b返回false。虽然不一定总是将这种情况更有意义地视为“相等”还是“不相等”,但仅根据“ ==”运算符定义相等的函数必须将它们视为“不相等” ”,无论哪种行为更有意义[如果我有德鲁特,所有类型都应使布尔型的“ ==”和“!=”运算符的行为保持一致,但是IEEE-754要求打破相等性这一事实运营商将使这种期望变得困难]。
超级猫

18
“从严格的语言角度来看,您所看到的代码不存在。” -当某个观点说某事不存在,但您实际上正在查看该事物时,则该观点是错误的。实际上,该标准并没有说代码不存在,只是没有说它确实存在:-)
Steve Jessop

@SteveJessop:您可以将“从严格的语言角度来看”表达式替换为“在语言级别上严格”。关键是,OP发布的代码与语言标准无关。
Christian Hackl

2
@supercat:IEEE-754确实让==!=行为一致,但我一直认为,所有六个关系应计算为false当至少一个操作数NaN
Ben Voigt

@BenVoigt:嗯,是的。它使两个运算符以相同的折断方式运行,从而使它们彼此一致,但仍设法违反与等价关系相关的所有其他常规公理(例如,它们既不支持a == a,也不保证执行的操作相等的值将产生相等的结果)。
超级猫

15

这里有一些好的答案。我只想添加一点笔记。

像所有好的库一样,标准库在设计时(至少)牢记​​两个非常重要的原则:

  1. 对您可以逃脱的图书馆用户承担最少的责任。这部分与使用界面时给他们最少的工作量有关。(例如,定义尽可能少的运算符)。它的另一部分与使它们不感到惊讶或要求它们检查错误代码有关(因此,请保持接口一致,并<stdexcept>在出现问题时引发异常)。

  2. 消除所有逻辑冗余。只能从推导出所有比较operator<,那么为什么要求用户定义其他人呢?例如:

    (a> b)等于(b <a)

    (a> = b)等于!(a <b)

    (a == b)等同于!((a <b)||(b <a))

    等等。

    当然,在此说明中,您可能会问为什么unordered_map要求operator==(至少默认情况下)而不是operator<。答案是,在哈希表中,我们所需要的唯一比较是相等性比较。因此,要求他们定义相等运算符在逻辑上更加一致(即对库用户更有意义)。要求operator<会造成混淆,因为尚不清楚为什么您需要它。


10
关于第二点:即使没有逻辑顺序,也可以在逻辑上比较相等性的类型,或者为它们建立顺序是非常人为的。例如,红色是红色,红色不是绿色,但是红色固有地小于绿色吗?
Christian Hackl

1
完全同意。因为没有逻辑顺序,所以不会将这些项目存储在有序容器中。它们可能更适合存储在需要operator==(和hash)的无序容器中。
理查德·霍奇斯

3.尽可能重载最不标准的运算符。这是他们实施的另一个原因!(a==b)。因为运算符的未反映的过载很容易导致C ++程序完全混乱(而且,使程序员发疯,因为调试他的代码可能成为不可能的任务,因为发现特定错误的元凶就像是一段漫游。)
语法错误

!((a < b) || (b < a))使用的布尔运算符少一些,所以它可能更快
Filip Haglund 2015年

1
实际上,它们不是。在汇编语言中,所有比较均实现为减法,然后测试标志寄存器中的进位和零位。其他所有内容只是语法糖。
理查德·霍奇斯

8

EqualityComparable概念需要operator==定义。

因此,任何自称可以使用满足类型要求的功能EqualityComparable 都不能依赖于该operator!=类型对象的存在。(除非存在暗示存在的其他要求operator!=)。


1

最有前途的方法是找到一种确定是否可以为特定类型调用operator ==的方法,然后仅在可用时才支持它。在其他情况下,将引发异常。但是,迄今为止,尚无已知方法来检测是否适当定义了任意运算符表达式f == g。已知的最佳解决方案具有以下不良质量:

  • 对于无法访问operator ==的对象(例如,因为它是私有的),在编译时失败。
  • 如果调用operator ==不明确,则在编译时失败。
  • 即使operator ==可能未编译,但如果operator ==声明正确,则似乎是正确的。

来自Boost FAQ:来源

知道要求==实施是一个负担,因此您永远不想通过要求!=实施。

对我个人而言,这是关于SOLID(面向对象设计)的L部分-Liskov替换原则:“程序中的对象应该可以用其子类型的实例替换,而不会改变该程序的正确性。” 在这种情况下,运算符!=可以替换为==和布尔逻辑中的布尔逆

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.