【问题标题】:Why don't C++ compilers define operator== and operator!=?为什么 C++ 编译器不定义 operator== 和 operator!=?
【发布时间】:2020-09-20 01:20:52
【问题描述】:

我非常喜欢让编译器为您做尽可能多的工作。在编写一个简单的类时,编译器可以“免费”为您提供以下内容:

  • 默认(空)构造函数
  • 复制构造函数
  • 析构函数
  • 赋值运算符 (operator=)

但它似乎无法为您提供任何比较运算符 - 例如 operator==operator!=。例如:

class foo
{
public:
    std::string str_;
    int n_;
};

foo f1;        // Works
foo f2(f1);    // Works
foo f3;
f3 = f2;       // Works

if (f3 == f2)  // Fails
{ }

if (f3 != f2)  // Fails
{ }

这样做有充分的理由吗?为什么执行逐个成员的比较会成为问题?显然,如果该类分配内存,那么您要小心,但是对于一个简单的类,编译器肯定可以为您做到这一点吗?

【问题讨论】:

  • 当然,析构函数也是免费提供的。
  • 在他最近的一次谈话中,Alex Stepanov 指出没有默认自动分配== 是错误的,就像有默认自动分配 (=)在一定条件下。 (关于指针的论点不一致,因为逻辑适用于===,而不仅仅是第二个)。
  • @becko,它是 A9 上的“使用组件进行高效编程”或“编程对话”系列中的第一个,可在 Youtube 上找到。
  • 查看此答案以获取 C++20 信息:stackoverflow.com/a/50345359

标签: c++ operators


【解决方案1】:

我同意,对于 POD 类型的类,编译器可以为您完成。但是,您可能认为简单的编译器可能会出错。所以还是让程序员来做比较好。

我曾经有一个 POD 案例,其中两个字段是唯一的 - 所以比较永远不会被认为是正确的。然而,我只需要在有效负载上进行比较 - 编译器永远无法理解或无法自己弄清楚。

此外,他们不会花很长时间来写吗?!

【讨论】:

  • 这并不是因为它们需要时间来编写,而是它们很容易把它们弄乱(或者当你在类中添加更多成员变量时忘记更新它们)。没有什么比花几个小时跟踪一个由 == 操作员忽略比较 POD 类的三个成员变量之一引起的运行时错误更有趣的了:/
【解决方案2】:

编译器不知道您想要的是指针比较还是深度(内部)比较。

不实现它并让程序员自己做会更安全。然后他们可以做出他们喜欢的所有假设。

【讨论】:

  • 这个问题并没有阻止它生成一个复制ctor,它是非常有害的。
  • 复制构造函数(和operator=)通常在与比较运算符相同的上下文中工作——也就是说,在您执行a = b 之后,a == b 是正确的。编译器使用与operator= 相同的聚合值语义来提供默认operator== 绝对是有意义的。我怀疑 paercebal 在这里实际上是正确的,因为 operator=(和复制 ctor)仅为 C 兼容性而提供,他们不想让情况变得更糟。
  • -1。当然你想要深度比较,如果程序员想要指针比较,他会写 (&f1 == &f2)
  • 维克多,我建议你重新考虑一下你的回答。如果 Foo 类包含 Bar*,那么编译器如何知道 Foo::operator== 是要比较 Bar* 的地址还是 Bar 的内容?
  • @Mark:如果它包含一个指针,比较指针值是合理的——如果它包含一个值,比较值是合理的。在特殊情况下,程序员可以重写。这就像语言实现了整数和指向整数的比较。
【解决方案3】:

C++0x has 有一个关于默认函数的提议,所以你可以说default operator==; 我们了解到,将这些内容明确化是有帮助的。

【讨论】:

  • 移动构造函数也可以默认,但我认为这不适用于operator==。真可惜。
【解决方案4】:

从概念上讲,定义平等并不容易。即使对于 POD 数据,有人可能会争辩说,即使字段相同,但它是不同的对象(在不同的地址),它也不一定相等。这实际上取决于运营商的使用情况。不幸的是,您的编译器不是通灵的,无法推断。

除此之外,默认函数是自爆的绝佳方法。您描述的默认值基本上是为了保持与 POD 结构的兼容性。然而,它们确实会造成足够多的破坏,让开发人员忘记它们或默认实现的语义。

【讨论】:

  • POD 结构没有歧义 - 它们的行为方式应该与任何其他 POD 类型完全相同,即值相等(而不是引用相等)。通过从另一个复制ctor创建的int等于创建它的那个;对于两个int 字段中的struct,唯一合乎逻辑的做法是以完全相同的方式工作。
  • @mgiuca:我可以看到通用等价关系非常有用,它允许任何作为值的类型用作字典或类似集合中的键。然而,如果没有保证自反的等价关系,这样的集合就无法发挥有用的作用。恕我直言,最好的解决方案是定义一个所有内置类型都可以合理实现的新运算符,并定义一些类似于现有指针类型的新指针类型,除了一些将相等定义为引用等价,而另一些则链接到目标的等价运算符。
  • @supercat 以此类推,您可以为 + 运算符提出几乎相同的论点,因为它与浮点数无关;那是 (x + y) + z != x + (y + z),由于 FP 舍入的方式。 (可以说,这是一个比== 更糟糕的问题,因为它适用于普通数值。)您可能会建议添加一个适用于所有数字类型(甚至是int)的新加法运算符,并且与@987654328 几乎完全相同@ 但它是关联的(不知何故)。但是,如果没有真正帮助那么多人,你就会给语言增加臃肿和混乱。
  • @mgiuca:除了边缘情况外,拥有非常相似的东西通常非常有用,而避免此类事情的错误努力会导致不必要的复杂性。如果客户端代码有时需要以一种方式处理边缘情况,有时需要以另一种方式处理它们,那么为每种处理方式都有一个方法将消除客户端中的大量边缘情况处理代码。至于你的类比,没有办法定义对固定大小的浮点值的操作以在所有情况下产生传递结果(尽管一些 1980 年代的语言有更好的语义......
  • ...在这方面比今天的),因此他们不做不可能的事这一事实应该不足为奇。然而,实现一个普遍适用于任何可以复制的值类型的等价关系并不存在根本性的障碍。
【解决方案5】:

无法定义默认==,但您可以通过通常应该自己定义的== 定义默认!=。 为此,您应该做以下事情:

#include <utility>
using namespace std::rel_ops;
...

class FooClass
{
public:
  bool operator== (const FooClass& other) const {
  // ...
  }
};

详情可以看http://www.cplusplus.com/reference/std/utility/rel_ops/

另外如果你定义了operator&lt; ,那么在使用std::rel_ops时,可以从中推导出、>=的操作符。

但是当你使用std::rel_ops时你应该小心,因为比较运算符可以推导出你不期望的类型。

从基本运算符推导出相关运算符的更优选方法是使用boost::operators

boost 中使用的方法更好,因为它为您只需要的类定义了运算符的用法,而不是针对范围内的所有类。

您还可以从“+=”生成“+”,从“-=”生成-等等...(参见完整列表here

【讨论】:

  • rel_ops 在 C++20 中被弃用是有原因的:因为it doesn't work,至少不是在所有地方,当然也不是始终如一。没有可靠的方法可以让sort_decreasing() 编译。另一方面,Boost.Operators 有效并且一直有效。
【解决方案6】:

如果编译器可以提供默认的复制构造函数,它应该能够提供类似的默认operator==() 的论点有一定的意义。我认为决定不为该运算符提供编译器生成的默认值的原因可以通过 Stroustrup 在“C++ 的设计和演变”(第 11.4.1 节 - 复制控制)中关于默认复制构造函数的说法来猜测。 :

我个人认为这很不幸 复制操作定义为 默认,我禁止复制 我的许多班级的对象。 但是,C++ 继承了它的默认值 赋值和复制构造函数 C,它们经常被使用。

因此,问题应该是“为什么 C++ 有默认赋值和复制构造函数?”而不是“为什么 C++ 没有默认的 operator==()?”,答案是这些项目不情愿地包含在Stroustrup 与 C 向后兼容(可能是大多数 C++ 缺陷的原因,但也可能是 C++ 流行的主要原因)。

出于我自己的目的,在我的 IDE 中,我用于新类的 sn-p 包含对私有赋值运算符和复制构造函数的声明,因此当我生成一个新类时,我不会得到默认赋值和复制操作 - 我有如果我希望编译器能够为我生成这些操作的声明,则从 private: 部分显式删除这些操作的声明。

【讨论】:

  • 好答案。我只想指出,在 C++11 中,与其将赋值运算符和复制构造函数设为私有,不如将它们完全删除,如下所示:Foo(const Foo&amp;) = delete; // no copy constructorFoo&amp; Foo=(const Foo&amp;) = delete; // no assignment operator
  • “但是,C++ 从 C 继承了它的默认赋值和复制构造函数” 这并不意味着您必须以这种方式创建所有 C++ 类型。他们应该把它限制在普通的旧 POD 中,只是 C 中已经存在的类型,没有更多。
  • 我当然可以理解为什么 C++ 继承了 struct 的这些行为,但我确实希望它让 class 表现得不同(并且理智地)。在这个过程中,除了默认访问之外,它还会在structclass 之间给出更有意义的区别。
【解决方案7】:

恕我直言,没有“好”的理由。之所以有这么多人同意这个设计决定,是因为他们没有学会掌握基于价值的语义的力量。人们需要编写大量自定义复制构造函数、比较运算符和析构函数,因为他们在实现中使用原始指针。

当使用适当的智能指针(如 std::shared_ptr)时,默认的复制构造函数通常很好,假设的默认比较运算符的明显实现也很好。

【讨论】:

    【解决方案8】:

    答案是 C++ 没有做 == 因为 C 没有,这就是为什么 C 只提供默认 = 而首先不提供 == 的原因。 C 想保持简单: C 实现 = 通过 memcpy;但是,由于填充,== 无法由 memcmp 实现。 因为填充没有初始化,所以 memcmp 说它们是不同的,即使它们是相同的。 空类也存在同样的问题:memcmp 说它们是不同的,因为空类的大小不为零。 从上面可以看出,实现 == 比在 C 中实现 = 更复杂。 关于此的一些代码example。 如果我错了,感谢您的指正。

    【讨论】:

    • C++ 不对operator= 使用 memcpy - 这仅适用于 POD 类型,但 C++ 也为非 POD 类型提供了默认的 operator=
    • 是的,C++ 以更复杂的方式实现了 =。似乎 C 只是用一个简单的 memcpy 实现了 =。
    【解决方案9】:

    videoAlex Stepanov 中,STL 的创建者在 13:00 左右解决了这个问题。总而言之,在观察了 C++ 的演变之后,他认为:

    • 不幸的是,== 和 != 没有被隐式声明(Bjarne 同意他的观点)。正确的语言应该为你准备好这些东西(他进一步建议你不应该定义一个破坏 == 语义的 !=
    • 出现这种情况的原因(与许多 C++ 问题一样)源于 C。在那里,赋值运算符是使用 逐位赋值 隐式定义的,但这不适用于 ==。可以在 Bjarne Stroustrup 的 article 中找到更详细的解释。
    • 在后续问题 为什么不使用逐个成员的比较,他说了一个令人惊奇的事情:C 是一种本土语言,是实现的人里奇的这些东西告诉他,他发现这很难实施!

    然后他说,在(遥远的)未来,==!= 将被隐式生成。

    【讨论】:

      【解决方案10】:

      即使在 C++20 中,编译器仍然不会为你隐式生成 operator==

      struct foo
      {
          std::string str;
          int n;
      };
      
      assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed
      

      但您将获得显式默认==since C++20的能力:

      struct foo
      {
          std::string str;
          int n;
      
          // either member form
          bool operator==(foo const&) const = default;
          // ... or friend form
          friend bool operator==(foo const&, foo const&) = default;
      };
      

      默认 == 执行成员方式 == (与默认复制构造函数执行成员方式复制构造的方式相同)。新规则还提供了==!= 之间的预期关系。例如,使用上面的声明,我可以写两个:

      assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
      assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!
      

      此特定功能(默认 operator== 以及 ==!= 之间的对称性)来自 one proposal,它是更广泛的语言功能 operator&lt;=&gt; 的一部分。

      【讨论】:

      • @dcmm88 不幸的是,它在 C++17 中不可用。我已经更新了答案。
      • 一个允许相同事物(除了简短形式)的修改提案将在 C++20 中出现:)
      • @artin 向语言添加新功能不应该破坏现有实现是有道理的。添加新的库标准或编译器可以做的新事情是一回事。在以前不存在的地方添加新的成员函数是完全不同的故事。为了保护您的项目免受错误的影响,需要付出更多的努力。我个人更喜欢编译器标志在显式和隐式默认值之间切换。您从较旧的 C++ 标准构建项目,使用编译器标志的显式默认值。您已经更新了编译器,因此您应该正确配置它。对于新项目,让它隐含。
      【解决方案11】:

      这样做有充分的理由吗?为什么逐个成员比较会成为问题?

      这在功能上可能不是问题,但在性能方面,默认的逐个成员比较可能比默认的逐个成员分配/复制更不理想。与分配顺序不同,比较顺序会影响性能,因为第一个不相等的成员意味着可以跳过其余的成员。因此,如果有一些成员通常相等,您希望最后比较它们,而编译器不知道哪些成员更有可能相等。

      考虑这个例子,其中verboseDescription 是从一组相对较小的可能天气描述中选择的长字符串。

      class LocalWeatherRecord {
          std::string verboseDescription;
          std::tm date;
          bool operator==(const LocalWeatherRecord& other){
              return date==other.date
                  && verboseDescription==other.verboseDescription;
          // The above makes a lot more sense than
           // return verboseDescription==other.verboseDescription
           //     && date==other.date;
          // because some verboseDescriptions are liable to be same/similar
          }
      }
      

      (当然,如果编译器认为它们没有副作用,则它有权忽略比较的顺序,但大概它仍然会从源代码中获取它的队列,因为它没有更好的信息。拥有。)

      【讨论】:

      • 但是,如果您发现性能问题,没有人会阻止您编写优化的用户定义比较。不过,根据我的经验,这只是极少数情况。
      【解决方案12】:

      C++20 提供了一种轻松实现默认比较运算符的方法。

      来自cppreference.com的示例:

      class Point {
          int x;
          int y;
      public:
          auto operator<=>(const Point&) const = default;
          // ... non-comparison functions ...
      };
      
      // compiler implicitly declares operator== and all four relational operators work
      Point pt1, pt2;
      if (pt1 == pt2) { /*...*/ } // ok, calls implicit Point::operator==
      std::set<Point> s; // ok
      s.insert(pt1); // ok
      if (pt1 <= pt2) { /*...*/ } // ok, makes only a single call to Point::operator<=>
      

      【讨论】:

      • 我很惊讶他们使用 Point 作为 ordering 操作的示例,因为没有合理的默认方式来使用 x 和 @ 订购两个点987654325@坐标...
      • @pipe 如果您不关心元素的顺序,使用默认运算符是有意义的。例如,您可以使用std::set 来确保所有点都是唯一的,而std::set 仅使用operator&lt;
      • 关于返回类型auto:对于这种情况,我们可以一直假设它是std::strong_ordering from #include &lt;compare&gt;吗?
      • @kevinarpe 返回类型为std::common_comparison_category_t,对于此类,它成为默认排序(std::strong_ordering)。
      【解决方案13】:

      只是为了让这个问题的答案随着时间的推移而保持完整:从 C++20 开始,它可以使用命令 auto operator&lt;=&gt;(const foo&amp;) const = default; 自动生成

      它将生成所有运算符:==、!=、 和 >=,详情请参阅https://en.cppreference.com/w/cpp/language/default_comparisons

      由于操作员的外观&lt;=&gt;,它被称为宇宙飞船操作员。另见Why do we need the spaceship <=> operator in C++?

      编辑:同样在 C++11 中,std::tie 提供了一个相当简洁的替代品,请参阅 https://en.cppreference.com/w/cpp/utility/tuple/tie 以获取带有 bool operator&lt;(…) 的完整代码示例。有趣的部分,改为使用== 是:

      #include <tuple>
      
      struct S {
      ………
      bool operator==(const S& rhs) const
          {
              // compares n to rhs.n,
              // then s to rhs.s,
              // then d to rhs.d
              return std::tie(n, s, d) == std::tie(rhs.n, rhs.s, rhs.d);
          }
      };
      

      std::tie 适用于所有比较运算符,并被编译器完全优化掉。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-08-30
        • 1970-01-01
        • 2011-02-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多