ChatGPT解决这个技术问题 Extra ChatGPT

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

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

默认(空)构造函数

复制构造函数

一个析构函数

赋值运算符 (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

p
pooya13

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

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

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

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


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

即使在 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<=> 的一部分。


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

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

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


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

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

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


R
Rio Wing

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


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

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

不幸的是 == 和 != 没有被隐式声明(并且 Bjarne 同意他的观点)。一个正确的语言应该为你准备好这些东西(他进一步建议你不应该定义一个破坏 == 语义的!=)

这种情况的原因在 C 中有其根源(与许多 C++ 问题一样)。在那里,赋值运算符是通过逐位赋值隐式定义的,但这不适用于 ==。可以在 Bjarne Stroustrup 的这篇文章中找到更详细的解释。

在后续问题中,为什么不使用成员之间的比较,他说了一件令人惊奇的事情:C 是一种本土语言,为 Ritchie 实现这些东西的人告诉他,他发现这很难实现!

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


B
Barry

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 操作的示例,因为没有合理的默认方式来使用 xy 坐标对两个点进行排序......
@pipe如果您不关心元素的顺序,则使用默认运算符是有意义的。例如,您可以使用 std::set 来确保所有点都是唯一的,而 std::set 仅使用 operator<
关于返回类型 auto:对于这种情况,我们是否可以始终假设它是来自 #include <compare>std::strong_ordering
@kevinarpe 返回类型是 std::common_comparison_category_t,对于此类,它成为默认排序 (std::strong_ordering)。
N
Nate Kohl

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

#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 中使用的方法更好,因为它为您只需要的类定义了运算符的用法,而不是针对范围内的所有类。

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


rel_ops 在 C++20 中被弃用是有原因的:因为 it doesn't work,至少不是无处不在,当然也不是一致的。没有可靠的方法来编译 sort_decreasing()。另一方面,Boost.Operators 有效并且一直有效。
M
MSalters

C++0x 对默认函数提出了建议,因此您可以说 default operator==; 我们了解到,明确这些内容会有所帮助。


也可以默认移动构造函数,但我认为这不适用于 operator==。这是一个遗憾。
P
Paul de Vrieze

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

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


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

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

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

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

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


M
Museful

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

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

考虑这个示例,其中 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
    }
}

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


但是,如果您发现性能问题,没有人会阻止您编写优化的用户定义比较。以我的经验,这将是极少数情况。
g
graham.reeds

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

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

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


并不是说它们需要时间来编写,而是它们很容易搞砸(或者在您向类中添加更多成员变量时忘记更新它们)。没有什么比花几个小时跟踪一个由 == 运算符忽略比较 POD 类的三个成员变量之一引起的运行时错误更有趣的了:/