334

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

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

但它似乎无法为您提供任何比较运算符 - 例如operator==or 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
{ }

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

4

13 回答 13

333

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

我个人认为很遗憾,默认情况下定义了复制操作,并且我禁止复制我的许多类的对象。但是,C++ 继承了 C 的默认赋值和复制构造函数,并且经常使用它们。

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

出于我自己的目的,在我的 IDE 中,我用于新类的代码段包含私有赋值运算符和复制构造函数的声明,因此当我生成一个新类时,我没有默认赋值和复制操作 - 我必须显式删除声明private:如果我希望编译器能够为我生成这些操作,请从该部分中提取这些操作。

于 2008-10-20T14:53:20.657 回答
115

即使在 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<=>

于 2015-01-08T10:23:14.293 回答
79

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

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

于 2008-10-20T09:53:07.190 回答
45

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

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

于 2009-04-03T17:05:05.167 回答
42

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

于 2011-12-07T16:50:56.110 回答
31

在这段视频中,STL 的创建者 Alex Stepanov 在大约 13:00 解决了这个问题。总而言之,在观察了 C++ 的演变之后,他认为:

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

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

于 2014-04-27T21:15:42.993 回答
22

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<=>
于 2018-05-15T08:11:51.827 回答
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 中使用的方法更好,因为它为您只需要的类定义了运算符的用法,而不是针对范围内的所有类。

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

于 2008-10-20T11:23:28.613 回答
11

C++0x一个关于默认函数的提议,所以你可以说default operator==; 我们已经知道它有助于使这些事情变得明确。

于 2008-10-20T09:59:31.607 回答
5

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

除此之外,默认功能是在脚下射击自己的绝佳方式。您描述的默认值基本上是为了保持与 POD 结构的兼容性。然而,它们确实造成了足够多的破坏,开发人员忘记了它们,或者默认实现的语义。

于 2008-10-20T10:06:15.447 回答
4

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

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

由于操作员的外观<=>,它被称为宇宙飞船操作员。另请参阅为什么我们需要 C++ 中的 spaceship <=> 运算符?.

编辑:在 C++11 中也有一个非常简洁的替代品,std::tie请参阅https://en.cppreference.com/w/cpp/utility/tuple/tie以获取完整的代码示例bool operator<(…)。更改为使用的有趣部分==是:

#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适用于所有比较运算符,并由编译器完全优化。

于 2019-09-06T12:29:07.837 回答
2

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

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

考虑这个例子,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
    }
}

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

于 2015-12-04T20:01:33.843 回答
0

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

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

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

于 2008-10-20T09:50:35.680 回答