为什么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
{ }

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

106821 次浏览

我同意,对于POD类型的类,编译器可以为你做。然而,你可能认为简单的编译器可能会出错。所以最好还是让程序员来做。

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

此外,他们不需要花很长时间来写,不是吗?!

编译器不会知道你想要的是指针比较还是深层(内部)比较。

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

c++ 0x 有一个默认函数的建议,所以你可以说default operator==;

.

.

.

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

除此之外,默认函数是搬起石头砸自己的脚的好方法。你描述的默认值基本上是为了保持与POD结构体的兼容性。然而,它们确实会造成足够多的破坏,让开发人员忘记它们或默认实现的语义。

不能定义默认的==,但是你可以通过==定义默认的!=,这通常是你自己定义的。 为此你应该做以下事情:

#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::运营商

boost中使用的方法更好,因为它为您只需要的类定义了operator的用法,而不是为作用域内的所有类定义了operator的用法。

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

如果编译器可以提供默认的复制构造函数,那么它应该能够提供类似的默认operator==(),这是有一定意义的。我认为决定不为该操作符提供编译器生成的默认构造函数的原因可以从Stroustrup在“c++的设计和发展”(第11.4.1节-复制的控制)中关于默认复制构造函数的描述中猜到:

我个人认为这很不幸 定义的复制操作 默认和我禁止复制 我的许多类的对象。 然而,c++继承了它的默认值 赋值和复制构造函数

. C

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

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

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

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

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

在这个< >强视频< / >强 Alex Stepanov中,STL的创造者在13:00左右解决了这个问题。总结一下,在见证了c++的发展之后,他认为:

  • 遗憾的是== and !=没有隐式声明(Bjarne同意他的观点)。正确的语言应该为你准备好这些东西(他进一步建议你不应该能够定义破坏= =语义的! =)。
  • 出现这种情况的原因(和许多c++问题一样)根源于C语言。在C语言中,赋值操作符是用逐位分配隐式定义的,但这对= =不起作用。更详细的解释可以在Bjarne Stroustrup的<强> < / >强条中找到。
  • 在接下来的问题为什么不使用成员对成员比较呢中,他说一个神奇的事情: C是一种本土语言,为Ritchie实现这些东西的人告诉他,他发现这很难实现!

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

即使在c++ 20中,编译器仍然不会隐式地为你生成operator==

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


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

但是你将获得显式地 default == 因为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<=>的一部分。

这有什么好的理由吗?为什么执行逐个成员的比较是一个问题?

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

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

(当然,如果编译器意识到比较没有副作用,它就有权忽略比较的顺序,但如果它自己没有更好的信息,它可能仍然会从源代码中获取查询。)

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

只是为了让这个问题的答案随着时间的推移保持完整:自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适用于所有比较操作符,并被编译器完全优化。