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

Rob*_*Rob 286 c++ operators

我非常喜欢让编译器为你做尽可能多的工作.在编写一个简单的类时,编译器可以为"free"提供以下内容:

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

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

Mic*_*urr 302

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

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

因此operator==(),问题应该是"为什么C++没有默认赋值和复制构造函数?",答案是Stroustrup不情愿地将这些项目包含在内,以便向后兼容C(可能是大多数C++瑕疵的原因,也可能是C++流行的主要原因).

出于我自己的目的,在我的IDE中,我用于新类的片段包含私有赋值运算符和复制构造函数的声明,这样当我生成一个新类时,我没有得到默认赋值和复制操作 - 我必须显式删除声明private:如果我希望编译器能够为我生成它们,那么该部分的操作.

  • 好答案.我只想指出在C++ 11中,不是将赋值运算符和复制构造函数设为私有,而是可以完全删除它们:`Foo(const Foo&)= delete; //没有复制构造函数`和`Foo&Foo =(const Foo&)=删除; //没有赋值运算符 (28认同)
  • "但是,C++从C继承了它的默认赋值和复制构造函数"这并不意味着你必须以这种方式制作所有C++类型.他们应该将此限制为普通的旧POD,只是C中的类型,不再是. (9认同)
  • 我当然可以理解,为什么C ++会为`struct`继承这些行为,但我希望它能使`class`表现出不同的行为(理智地)。在此过程中,除了默认访问权限外,它还会在“ struct”和“ class”之间提供更有意义的区别。 (2认同)

Ant*_*vin 78

更新2:不幸的是,这个提议没有进入C++ 17,所以现在这方面没有任何改变.

更新:它具有很高的几率获得投入C++ 17提案的当前版本是在这里.

最近有一个关于明确默认的比较运算符的提议(N4126),它得到了标准委员会非常积极的反馈,所以希望我们能在C++ 17中以某种形式看到它.

简而言之,建议的语法是:

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

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed
Run Code Online (Sandbox Code Playgroud)

或者operator==以私有字段的类的形式:

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;
};
Run Code Online (Sandbox Code Playgroud)

甚至是简短的形式:

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!
Run Code Online (Sandbox Code Playgroud)

当然,这个提案最终被接受的时候可能会改变这一切.

  • @ dcmm88很遗憾它在C++ 17中不可用.我已经更新了答案. (3认同)
  • 经过修改的提案允许相同的内容(简短形式除外)将在C ++ 20中实现:) (2认同)
  • @artin 这是有道理的,因为向语言添加新功能不应破坏现有的实现。添加新的库标准或编译器可以做的新事情是一回事。在以前不存在的地方添加新的成员函数是完全不同的事情。为了确保您的项目免遭错误,需要付出更多的努力。我个人更喜欢编译器标志在显式默认值和隐式默认值之间切换。您可以根据较旧的 C++ 标准构建项目,并通过编译器标志使用显式默认值。您已经更新了编译器,因此您应该正确配置它。对于新项目,将其隐式化。 (2认同)

Mar*_*ram 72

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

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

  • 这个问题并没有阻止它产生一个复制ctor,它是非常有害的. (276认同)
  • 复制构造函数(和`operator =`)通常在与比较运算符相同的上下文中工作 - 也就是说,期望在执行`a = b`之后,`a == b`为真.对于编译器来说,使用与`operator =`相同的聚合值语义来提供默认的`operator ==`是绝对有意义的.我怀疑paercebal在这里是正确的,因为`operator =`(和copy ctor)仅仅是为了兼容C而提供的,并且他们不想让情况变得更糟. (73认同)
  • 维克多,我建议你重新考虑一下你的回答.如果类Foo包含一个Bar*,那么编译器如何知道Foo :: operator ==是否要比较Bar*的地址或Bar的内容? (58认同)
  • @Mark:如果它包含一个指针,比较指针值是合理的 - 如果它包含一个值,比较这些值是合理的.在特殊情况下,程序员可以覆盖.这就像语言实现了int和pointer-to-int之间的比较. (40认同)
  • -1.当然你需要深入比较,如果程序员想要一个指针比较,他会写(&f1 ==&f2) (39认同)
  • -1如其他人所指出的,当与operator =进行比较时,这不是一致的参数 (13认同)
  • 存在与C兼容的问题:C89为结构化代码模仿C++的分配(并且可能复制构造函数......我必须检查).因此,正常的C++生成类似的代码. (12认同)
  • @MarkIngram:你的话毫无意义.如果可访问的话,编译器实现的operator ==应该只调用所有成员的运算符==.所以例如clone_ptr不会破坏深链.std :: vector也不会.然而指针和智能指针会比较浅. (9认同)
  • 所述原因均无效.如果它说编译器必须提供一个默认的'operator =='来比较每个字段值,那么这不会是c ++标准中最疯狂的事情.如果我们想要一个针对指针的安全性,那么只是在这种情况下发出警告或错误. (6认同)
  • 编译器不需要弄清楚.可以将两个选项中的一个指定为标准,然后编译器供应商必须遵循该标准.当然,显而易见的选择是指针的浅层比较,以及成员的值比较,正如v.oddou所描述的那样. (3认同)
  • 这种不一致在标准委员会中仍然存在。在 C++ 20 中,他们改变了主意。现在您可以请求默认比较运算符(在强排序条件下)。 (2认同)

ale*_*xk7 42

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

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


Rio*_*ing 36

答案是C++没有做= =因为C没有,这就是为什么C在第一个地方只提供default =但是没有==.C希望保持简单:C实现= memcpy; 但是,由于填充,memcmp无法实现==.因为padding没有初始化,所以memcmp说它们是不同的,即使它们是相同的.空类存在同样的问题:memcmp表示它们不同,因为空类的大小不为零.从上面可以看出,实现==比在C中实现=更复杂.关于此的一些代码示例.如果我错了,你会得到更正.

  • C++不使用memcpy作为`operator =` - 这只适用于POD类型,但C++也为非POD类型提供了一个默认的`operator =`. (6认同)
  • 是的,C ++以更复杂的方式实现了=。似乎C只是用一个简单的memcpy实现了=。 (2认同)

Nik*_*iou 24

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

  • 不幸的是,==和!=没有被隐含声明(Bjarne同意他的意见).一个正确的语言应该为你准备好这些东西(他进一步建议你不应该定义一个!=打破==的语义)
  • 这种情况的原因在于其根源(如同许多C++问题)在C中.赋值运算符是通过逐位赋值隐式定义的,但这对于==不起作用.Bjarne Stroustrup 在本文中可以找到更详细的解释.
  • 在后续问题中为什么当时不是成员比较使用的成员他说了一个惊人的事情:C是一种本土语言,为Ritchie实施这些东西的人告诉他,他发现这很难实现!

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

  • 看起来这个遥远的未来不会是2017年,也不会是18年,也不是19年,你抓住了我的漂移...... (2认同)

ser*_*gtk 15

无法定义默认值==,但您可以定义默认值,您通常应该!=通过==它来定义自己.为此你应该做以下事情:

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

class FooClass
{
public:
  bool operator== (const FooClass& other) const {
  // ...
  }
};
Run Code Online (Sandbox Code Playgroud)

有关详细信息,请参阅http://www.cplusplus.com/reference/std/utility/rel_ops/.

此外,如果您定义operator< ,使用时可以从中推导出<=,>,> =的运算符std::rel_ops.

但是在使用时应该小心,std::rel_ops因为可以推导出比较运算符的类型,而不是预期的类型.

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

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

您还可以从"+ ="生成"+", - 从" - ="等生成...(请参阅此处的完整列表)

  • 有一个原因在C ++ 20中不推荐使用rel_ops:因为[它不起作用](https://godbolt.org/z/o0U3JS),至少不是在所有地方,而且肯定是不一致的。没有可靠的方法来使sort_decreasing()编译。另一方面,[Boost.Operators](https://godbolt.org/z/FJdMIz)有效并且一直有效。 (2认同)

vll*_*vll 15

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<=>
Run Code Online (Sandbox Code Playgroud)

  • @pipe如果您不关心元素的顺序,使用默认运算符是有意义的.例如,您可以使用`std :: set`来确保所有点都是唯一的,而`std :: set`仅使用`operator <`. (4认同)
  • 我很惊讶他们以“点”作为_ordering_操作的示例,因为没有合理的默认方式来订购带有“ x”和“ y”坐标的两个点。 (3认同)
  • 关于返回类型“auto”:对于*这种情况*,我们是否可以始终假设它将是“#include &lt;compare&gt;”中的“std::strong_ordering”? (2认同)
  • @kevinarpe 返回类型是“std::common_comparison_category_t”,对于此类来说,它成为默认排序(“std::strong_ordering”)。 (2认同)

MSa*_*ers 10

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

  • 移动构造函数也可以是默认的,但我不认为这适用于`operator ==`.哪个是遗憾. (4认同)
  • 我认为只有"特殊成员函数"(默认构造函数,复制构造函数,赋值运算符和析构函数)才能显式默认.他们是否将此扩展到其他一些运营商? (3认同)

Pau*_*eze 5

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

除此之外,默认功能是拍摄自己的好方法.您描述的默认值基本上是为了保持与POD结构的兼容性.然而,它们确实会导致开发人员忘记它们或者默认实现的语义.

  • POD结构没有歧义 - 它们的行为应与任何其他POD类型完全相同,即值相等(而不是引用相等).通过copy ctor从另一个创建的`int`等于创建它的那个; 对于两个`int`字段的`struct`,唯一合乎逻辑的做法是以完全相同的方式工作. (10认同)

cos*_*rgi 5

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

\n\n

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

\n\n

由于操作员的外貌<=>,被称为飞船操作员。另请参阅为什么我们需要 C++ 中的太空船 <=> 运算符?

\n\n

编辑:同样在 C++11 中,可以使用std::tiehttps://en.cppreference.com/w/cpp/utility/tuple/tie获取完整的代码示例bool operator<(\xe2\x80\xa6)。有趣的部分,更改为使用的==是:

\n\n
#include <tuple>\n\nstruct S {\n\xe2\x80\xa6\xe2\x80\xa6\xe2\x80\xa6\nbool operator==(const S& rhs) const\n    {\n        // compares n to rhs.n,\n        // then s to rhs.s,\n        // then d to rhs.d\n        return std::tie(n, s, d) == std::tie(rhs.n, rhs.s, rhs.d);\n    }\n};\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n