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


302

我非常乐于让编译器为您完成尽可能多的工作。在编写简单的类时,编译器可以为您提供以下“免费”功能:

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

是否有充分的理由呢?为什么进行逐成员比较会是一个问题?显然,如果该类分配了内存,那么您要格外小心,但是对于简单的类,编译器肯定可以为您执行此操作吗?


4
当然,析构函数也是免费提供的。
约翰·杰雷尔

23
在最近的一次演讲中,Alex Stepanov指出,没有==默认的自动分配是一个错误,就像=在某些条件下存在默认的自动分配()一样。(关于指针的参数是不一致的,因为逻辑既适用于===,也适用于第二种)。
alfC

2
@becko这是A9系列的一个视频:youtube.com/watch? v=k-meLQaYP5Y ,我不记得是哪一次演讲了。还有一个建议,似乎正在进入C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC

1
@becko,它是Youtube上可用的A9系列“使用组件进行高效编程”或“编程对话”系列中的第一个。
alfC

1
@becko实际上下面有一个答案指向了Alex的观点stackoverflow.com/a/23329089/225186
alfC

Answers:


71

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

只执行而不让程序员自己做是比较安全的。然后他们可以做出所有喜欢的假设。


292
这个问题并不能阻止它生成一个非常有害的副本ctor。
MSalters

78
复制构造函数(和operator=)通常在与比较运算符相同的上下文中工作-也就是说,期望执行a = ba == b为true。对于编译器,operator==使用与其相同的聚合值语义来提供默认值绝对是有意义的operator=。我怀疑paercebal在这里实际上是正确的,因为operator=(和复制ctor)仅用于C兼容性,并且他们不想使情况更糟。
帕维尔·米纳夫

46
-1。当然,您需要深度比较,如果程序员希望进行指针比较,他会写(&f1 ==&f2)
Viktor Sehr 2010年

62
维克多,我建议您重新考虑一下您的回应。如果Foo类包含Bar *,那么编译器将如何知道Foo :: operator ==是要比较Bar *的地址还是Bar的内容?
马克·英格拉姆

46
@Mark:如果包含指针,则比较指针值是合理的-如果包含值,则比较值是合理的。在特殊情况下,程序员可以重写。就像该语言在int和指向int的指针之间实现比较一样。
伊蒙·纳邦

317

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

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

因此operator==(),问题应该是“为什么C ++具有默认的赋值和复制构造函数?”,而不是“为什么C ++没有默认的?”,答案是Stroustrup勉强地包含了这些项,以便与C向后兼容。 (可能是大多数C ++疣的原因,但也可能是C ++普及的主要原因)。

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


29
好答案。我想指出的是,在C ++ 11中,您可以像这样完全删除它们,而不是将赋值运算符和复制构造函数Foo(const Foo&) = delete; // no copy constructorFoo& Foo=(const Foo&) = delete; // no assignment operator
设为

9
“但是,C ++继承了C的默认赋值并复制了C的构造函数。”这并不意味着您必须用这种方法来制作所有C ++类型。他们应该只将其限制为普通的旧POD,而不仅仅是C中的类型。
thesaint 2014年

3
我当然可以理解为什么C ++继承了的这些行为struct,但是我希望它让class行为有所不同(并且理智地)。在这个过程中,它也将给予之间的更有意义的差异structclass默认访问旁边。
jamesdlin

@jamesdlin如果需要规则,禁用ctor的隐式声明和定义以及在声明dtor时的赋值将是最有意义的。
重复数据删除器

1
我仍然认为让程序员明确命令编译器创建一个程序没有什么害处operator==。此时,这只是一些样板代码的语法糖。如果您担心程序员可能会忽略类字段中的某些指针,则可以添加条件,使其只能在本身具有相等运算符的原始类型和对象上工作。但是,没有理由完全禁止这样做。
NO_NAME

93

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


您知道这方面是否有最新更新吗?它可以在c ++ 17中使用吗?
dcmm88

3
@ dcmm88不幸的是,它将无法在C ++ 17中使用。我已经更新了答案。
安东·萨文

2
经过修改的提案允许使用相同的内容(简短形式除外)将在C ++ 20中实现:)
Rakete1111 '19

因此,基本上,您必须为= default默认情况下未创建的东西指定,对吗?在我看来,这听起来很矛盾(“默认默认”)。
artin

@artin很有意义,因为向语言添加新功能不应破坏现有的实现。添加新的库标准或编译器可以做的新事情是一回事。在以前不存在的成员函数上添加新成员完全是另一回事。为了使您的项目免于错误,需要付出更多的努力。我个人更希望编译器标志在显式和隐式默认之间​​切换。您从较早的C ++标准构建项目,并通过编译器标志使用显式默认值。您已经更新了编译器,因此应正确配置它。对于新项目,请使其隐式。
MaciejZałucki

44

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

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


39

答案是C ++没做==,因为C没做,这就是为什么C首先只提供default =而不提供==的原因。C希望保持简单:C实现=通过memcpy; 但是,由于填充,memcmp无法实现==。由于填充未初始化,因此memcmp表示它们是不同的,即使它们相同。空类存在相同的问题:memcmp表示它们不同,因为空类的大小不为零。从上面可以看出,实现==比在C中实现=更复杂。一些与此相关的代码示例。如果我错了,请多多指教。


6
C ++不将memcpy用于operator=-仅适用于POD类型,但C ++也operator=为非POD类型提供了默认值。
Flexo

2
是的,C ++以更复杂的方式实现了=。看来C只是用一个简单的memcpy实现的。
里约热内卢

此答案的内容应与迈克尔的答案放在一起。他纠正问题,然后回答。
Sgene9

27

在此视频中,STL的创建者Alex Stepanov在13:00左右解决了这个问题。总而言之,在观察了C ++的发展之后,他认为:

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

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


2
似乎这个遥远的未来不会是2017或18或19,好吧,我赶上了...
UmNyobe

18

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

4
我很惊讶它们被Point用作订购操作的示例,因为没有合理的默认方式来订购带有xy坐标的两个点...
管道

4
@pipe如果您不在乎元素的顺序,则使用默认运算符很有意义。例如,您可能std::set用来确保所有点都是唯一的并且仅std::set使用operator<
vll

关于返回类型auto:在这种情况下,我们可以始终假设它std::strong_ordering来自#include <compare>吗?
kevinarpe

1
@kevinarpe返回类型为std::common_comparison_category_t,对于此类来说,其返回类型为默认排序(std::strong_ordering)。
vll

15

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

#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中使用的方法更好,因为它为仅想要的类而不是作用域中的所有类定义了运算符的用法。

您还可以从“ + =”生成“ +”,从-=生成-,等等...(在此处查看完整列表)


!=编写==运算符后,我没有得到默认设置。或者我做到了,但是那是缺乏const精神。也必须自己写,一切都很好。
约翰

您可以保持稳定玩耍以获得所需的结果。没有代码,很难说出问题所在。
sergtk,2012年

2
有一个原因rel_ops在C ++ 20中被弃用:因为它不起作用,至少不是在所有地方都起作用,而且肯定不是始终如一的。没有可靠的sort_decreasing()编译方法。另一方面,Boost.Operators工作并且一直在工作。
巴里(Barry)

10

的C ++ 0x 已经有默认功能的建议,所以你可以说default operator==; 我们已经了解到,它有助于使这些东西明确。


3
我认为只有“特殊成员函数”(默认构造函数,复制构造函数,赋值运算符和析构函数)可以显式默认。他们是否将此扩展到其他一些运营商?
Michael Burr

4
Move构造函数也可以默认设置,但我认为这不适用于operator==。真可惜。
帕维尔·米纳夫

5

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

除此之外,默认功能是脚踩自己的好方法。您描述的默认值基本上可以保证与POD结构的兼容性。但是,它们的确给开发人员造成了极大的破坏,使他们忘记了它们或默认实现的语义。


10
POD结构没有歧义-它们应该以任何其他POD类型完全相同的方式工作,即值相等(而不是引用相等)。一个int通过拷贝构造函数从另一个创建等于从它被创建的一个; struct对两个int字段中的a唯一要做的逻辑就是以完全相同的方式工作。
帕维尔·米纳夫

1
@mgiuca:对于通用的等价关系,我可以看到它非常有用,它可以将任何表现为值的类型用作字典或类似集合中的键。但是,如果没有保证的自反等价关系,此类集合就无法发挥有用的作用。恕我直言,最好的解决方案是定义一个新的运算符,所有内置类型都可以合理地实现,并定义一些新的指针类型,这些指针类型与现有的指针类型类似,不同之处在于有些将相等性定义为引用等效项,而另一些将链接到目标的等价运算符。
2015年

1
@supercat通过类推,您可以为+运算符设定几乎相同的参数,因为它与浮点数无关。由于FP舍入的方式,即(x + y) + z!= x + (y + z)。(可以说,这是一个比==正常数值更严重的问题。)您可能建议添加一个新的加法运算符,该运算符适用于所有数值类型(甚至是int),并且几乎+与之完全相同,但是具有关联性(不知何故)。但是那样的话,您将在没有真正帮助那么多人的情况下给语言增加膨胀和混乱的感觉。
mgiuca

1
@mgiuca:除了少数情况外,拥有非常相似的事物通常非常有用,而为避免此类事情而进行的错误引导会导致不必要的复杂性。如果客户端代码有时需要用一种方式处理边缘情况,有时又需要用另一种方式处理边缘情况,那么为每种处理方式提供一种方法将消除客户端中的许多边缘情况处理代码。就您的类比而言,无法在所有情况下都对固定大小的浮点值定义操作以产生传递结果(尽管某些1980年代的语言具有更好的语义……
supercat

1
...在这方面比今天要多),因此他们没有做不可能的事情就不足为奇了。但是,实现等价关系并没有普遍的障碍,这种等价关系将普遍适用于任何可以复制的价值类型。
超级猫

1

是否有充分的理由呢?为什么进行逐成员比较会是一个问题?

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

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

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


但是,如果发现性能问题,没有人会阻止您编写优化的用户定义比较。以我的经验,那只是少数案例。
彼得-恢复莫妮卡

1

只是为了使这个问题的答案随着时间的流逝保持完整:由于C ++ 20可以使用命令自动生成 auto operator<=>(const foo&) const = default;

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

由于操作员的外观<=>,它称为太空飞船操作员。另请参见为什么在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 与所有比较运算符一起使用,并且完全被编译器优化。


-1

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

我曾经有过一次POD案例,其中两个字段是唯一的-因此,比较永远不会被认为是正确的。但是,我只需要在有效负载上进行比较,这是编译器永远无法理解或无法自行解决的。

此外-他们不需要花很多时间来写吗?

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.