为什么C++编译器没有定义operator==和operator!=?

360

我非常支持让编译器尽可能地为你做更多工作。当编写一个简单类时,编译器可以免费为您提供以下内容:

  • 默认(空)构造函数
  • 复制构造函数
  • 析构函数
  • 赋值运算符(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
{ }

这是有什么好的理由吗?为什么逐成员比较会有问题?显然,如果类分配内存,那么你需要小心,但对于一个简单的类,编译器肯定可以为你完成这个任务,不是吗?


6
当然,析构函数也是免费提供的。 - Johann Gerell
39
在他最近的演讲中,Alex Stepanov指出,没有一个默认自动的==是一个错误,就像在某些条件下有一个默认自动分配(=)一样。(关于指针的论点是不一致的,因为这个逻辑既适用于=,也适用于==,而不仅仅是后者。) - alfC
3
@becko,这是A9公司“高效组件编程”系列或“编程对话”系列中的其中一集,可以在Youtube上观看。 - alfC
2
请参考此答案了解C++20信息:https://dev59.com/dnVC5IYBdhLWcg3wqzLV#50345359 - VLL
我的理由列表 - https://dev59.com/m2025IYBdhLWcg3w4qEx#5740505 - Tony Delroy
13个回答

347
那个认为如果编译器可以提供默认的复制构造函数,那么它也应该能够提供一个类似的默认operator==()的论点有一定道理。我认为不提供编译器生成的此运算符的默认值的决定可以通过Stroustrup在《C++设计与演化》(第11.4.1节-控制复制)中所说的关于默认复制构造函数的内容猜测:

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

因此,问题不应该是“为什么C++没有默认的operator==()?”而应该是“为什么C++有默认的赋值和复制构造函数?”,答案是这些项目是出于对C向后兼容性的迫切需要而由Stroustrup勉强包含的(可能是C++大部分缺点的原因,但也可能是C++流行的主要原因)。

对于我的目的,在我的IDE中,我使用的新类片段包含对私有赋值运算符和复制构造函数的声明,以便当我生成新类时,我不会得到默认的赋值和复制操作 - 如果我想让编译器为我生成它们,我必须明确地从private:部分删除这些操作的声明。


36
很好的回答。我想指出,在C++11中,你可以像这样完全删除赋值运算符和复制构造函数,而不是将它们设为私有:Foo(const Foo&) = delete; // 没有复制构造函数Foo& Foo=(const Foo&) = delete; // 没有赋值运算符 - karadoc
12
然而,C++从C语言继承了默认的赋值和拷贝构造函数,但这并不意味着你必须将所有C++类型都这样设计。他们本应该只将这种机制限制在POD(Plain Old Data)类型上,也就是那些已经在C语言中存在的类型,而不再扩展到其他类型上。 - thesaint
6
我理解为什么C++会沿用这些 struct 的行为,但我希望它可以让 class 表现得有所不同(并且更合理)。这样一来,除了默认访问权限之外,structclass 之间也会有更明显的区别。 - jamesdlin

129

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

struct foo
{
    std::string str;
    int n;
};

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed

但是你将获得能够在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====!=之间的对称性)来自于一个提案,该提案是更大语言特性的一部分,即operator<=>


4
@dcmm88 不幸的是,它不会在C++17中可用。我已经更新了答案。 - Anton Savin
3
修改后的提案将允许相同的事情(除了短表单),这将在C++20中出现。 :) - Rakete1111
2
@artin 这是有道理的,因为添加新功能到语言中不应该破坏现有的实现。添加新的库标准或编译器可以做的新事情是一回事。在以前不存在成员函数的地方添加新的成员函数则完全是另一回事。为了保护您的项目免受错误的影响,需要付出更多的努力。我个人更喜欢编译器标志来切换显式和隐式默认值。您可以使用编译器标志从旧的C++标准构建项目,并使用显式默认值。您已经更新了编译器,所以应该正确配置它。对于新项目,请将其设置为隐式。 - Maciej Załucki

82

编译器不知道你是想进行指针比较还是深度(内部)比较。

最保险的方法就是不去实现它,让程序员自己去实现。这样他们就可以做任何他们想要的假设。


336
这个问题并没有阻止它生成一个复制构造函数,但这会带来相当大的风险。 - MSalters
90
复制构造函数(和 operator=)通常在与比较运算符相同的上下文中工作 - 也就是说,在执行 a = b 后,期望 a == b 为真。编译器提供默认的 operator== 使用相同的聚合值语义绝对有意义。我怀疑 paercebal 在这里实际上是正确的,即 operator=(和复制构造函数)仅提供 C 兼容性,并且他们不想让情况变得更糟。 - Pavel Minaev
52
当然,如果程序员想要进行指针比较,他会写(&f1 == &f2)。 - Viktor Sehr
71
维克托,我建议你重新思考一下回答。如果类 Foo 包含一个 Bar,那么编译器如何知道 Foo::operator== 是想比较 Bar 的地址还是 Bar 的内容? - Mark Ingram
58
如果它包含一个指针,比较指针值是合理的——如果它包含的是一个值,比较值也是合理的。在非常规情况下,程序员可以进行覆盖。这就像语言实现int和pointer-to-int之间的比较一样。 - Eamon Nerbonne
显示剩余10条评论

48

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

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


其他答案试图通过与C进行比较或提供解决方法来证明缺少自动定义的operator==,但这是真正的答案。这只是糟糕的设计。默认情况下,比较应该实际上递归级联。 - phinz

45

回答是C++没有使用==运算符是因为C语言没有这个运算符,而C提供了默认的=运算符但没有提供==运算符。 C语言希望保持简单: C语言通过memcpy来实现=运算符;然而,由于填充,无法通过memcmp来实现==运算符。 由于填充不会被初始化,即使它们相同,memcmp也会认为它们是不同的。 对于空类的情况也存在同样的问题:由于空类的大小不为零,memcmp会认为它们是不同的。 由上可见,在C语言中实现==比实现=更加复杂。 有关此内容的一些代码示例。 如果我有错误,请指出。


8
C++不使用memcpy来实现operator=赋值运算符,因为这种方式只适用于POD类型,但对于非POD类型,C++提供了默认的operator=赋值运算符。 - Flexo
4
是的,C++ 的实现更加复杂。似乎 C 只是用简单的 memcpy 实现了赋值(=)。 - Rio Wing

35

在这个视频中,STL的创造者Alex Stepanov在大约13:00回答了这个问题。总结一下,他观察了C++的演变,认为:

很不幸的是,==!=没有被隐式声明(Bjarne也同意这一点)。一个正确的语言应该为你准备好这些东西(他进一步建议你不应该定义一个破坏==语义的!=)。
这种情况的原因可以追溯到C语言(就像许多C++问题一样)。在那里,赋值运算符隐式定义为逐位赋值,但这对于==不起作用。更详细的解释可以在Bjarne Stroustrup的文章中找到。
在后续问题为什么没有使用成员逐个比较中,他说了一件惊人的事情:C有点像自制的语言,实现这些东西的人告诉Ritchie他发现这很难实现!
然后他说,在(遥远的)未来,==!=将被隐式生成。

1
我在此声明这是真正被接受的答案。 - Sz.

24

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<=>

9
我很惊讶他们将“点(Point)”作为一个_排序(ordering)_操作的例子,因为没有合理的默认方法来对具有xy坐标的两个点进行排序... - pipe
5
如果你不关心元素的顺序,使用默认运算符是有意义的。例如,你可以使用 std::set 来确保所有点都是唯一的,而 std::set 只使用 operator< - VLL
2
关于返回类型auto: 对于这种情况,我们是否总是可以假设它来自于#include <compare>中的std::strong_ordering - kevinarpe
3
返回类型是 std::common_comparison_category_t,对于这个类来说,它成为了默认的排序方式(std::strong_ordering)。 - VLL

16

无法定义默认的==,但是可以通过==定义默认的!=,通常应该自己定义。
为此,您应该执行以下操作:

#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<,则可以从中推断出 <=、>、>= 的操作符,当使用 std::rel_ops时。

但是,当您使用std::rel_ops时应该注意,因为比较运算符可能会针对您所不希望的类型进行推导。

从基本操作符中推导相关操作符的更优选方式是使用boost::operators

Boost 中使用的方法更好,因为它仅为您想要的类定义了操作符的用法,而不是作用于所有范围内的类。

您还可以从“+=”生成“+”,从“-=”生成“-”等(完整列表请参见这里)。


5
rel_ops在C++20中被弃用的原因是它并不总是有效,至少不能在所有情况下,也不稳定。没有可靠的方法使得sort_decreasing()能够编译。另一方面,Boost.Operators一直以来都有效,并且可以使用。 - Barry

11

C++0x提出了默认函数的建议,因此您可以使用default operator==;来表示。

我们已经学到,将这些内容明确化有助于使其更加易懂。


5
移动构造函数也可以被默认,但我认为这不适用于operator==。这很遗憾。 - Pavel Minaev

5

概念上,定义相等并不容易。即使对于POD数据,人们也可以认为即使字段相同,但如果是不同的对象(在不同的地址),它也不一定相等。这实际上取决于运算符的使用。不幸的是,您的编译器不是心理学家,无法推断出这一点。

除此之外,默认函数是自我毁灭的绝佳方式。您所描述的默认值基本上是为了保持与POD结构的兼容性。然而,它们确实会在开发人员忘记它们或默认实现的语义时造成足够的混乱。


11
对于POD结构体,没有歧义 - 它们应该和任何其他POD类型一样行为,即值相等(而不是引用相等)。一个通过复制构造函数从另一个创建的int等于它所创建的那个;对于一个具有两个int字段的struct来说,唯一合理的做法就是以完全相同的方式工作。 - Pavel Minaev
1
@mgiuca:我可以看到一个通用等价关系的相当大的实用性,它允许任何表现为值的类型被用作字典或类似集合中的键。然而,这样的集合在没有保证自反等价关系的情况下无法有用地行为。在我看来,最好的解决方案是定义一个新的运算符,所有内置类型都可以明智地实现,并定义一些新的指针类型,它们就像现有的指针类型一样,只是有些会将相等性定义为引用等价性,而其他一些则会链接到目标的等价操作符。 - supercat
1
@supercat 类比来说,你可以对 + 运算符做出几乎相同的论证,即对于浮点数它是非结合的;也就是说 (x + y) + z != x + (y + z),这是由于 FP 舍入发生的方式。 (可以说,这比 == 更糟糕,因为它适用于普通数字值。)您可能会建议添加一个新的加法运算符,它适用于所有数字类型(甚至 int),并且几乎与 + 相同,但是它是结合的(某种程度上)。但是,这样做会向语言中添加膨胀和混乱,而实际上并没有帮助太多人。 - mgiuca
1
@mgiuca:在边缘情况下非常相似的事物通常非常有用,而试图避免这些事物的误导性努力会导致很多不必要的复杂性。如果客户端代码有时需要以一种方式处理边缘情况,有时需要以另一种方式处理它们,那么为每种处理样式设置一个方法将消除客户端中大量的边缘情况处理代码。至于您的类比,没有办法定义固定大小的浮点值的操作,以在所有情况下产生可传递的结果(尽管一些20世纪80年代的语言具有更好的语义... - supercat
1
相对于今天,它们在这方面的表现要好得多,因此它们不做不可能的事情也不应该让人感到惊讶。然而,没有根本性的障碍可以实现一个等价关系,这个关系可以普遍适用于任何可以复制的值类型。 - supercat
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接