我非常喜欢让编译器为你做尽可能多的工作。在编写一个简单的类时,编译器可以“免费”为您提供以下内容:
默认(空)构造函数
复制构造函数
一个析构函数
赋值运算符 (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
{ }
这有充分的理由吗?为什么执行逐个成员的比较会成为问题?显然,如果该类分配内存,那么您要小心,但是对于一个简单的类,编译器肯定可以为您做到这一点吗?
==
是错误的,就像在某些条件下有默认的自动分配 (=
)。 (关于指针的论点是不一致的,因为逻辑适用于 =
和 ==
,而不仅仅适用于第二个)。
如果编译器可以提供默认的复制构造函数,它应该能够提供类似的默认 operator==()
的论点是有一定意义的。我认为决定不为该运算符提供编译器生成的默认值的原因可以通过 Stroustrup 在“C++ 的设计和演变”(第 11.4.1 节 - 复制控制)中关于默认复制构造函数的说法来猜测:
我个人认为很遗憾,默认情况下定义了复制操作,并且我禁止复制我的许多类的对象。但是,C++ 继承了 C 的默认赋值和复制构造函数,并且经常使用它们。
因此,问题应该是“为什么 C++ 有默认赋值和复制构造函数?”而不是“为什么 C++ 没有默认 operator==()
?”,答案是这些项目被 Stroustrup 不情愿地向后包含与 C 的兼容性(可能是大多数 C++ 缺陷的原因,但也可能是 C++ 流行的主要原因)。
出于我自己的目的,在我的 IDE 中,我用于新类的代码段包含私有赋值运算符和复制构造函数的声明,因此当我生成一个新类时,我没有默认赋值和复制操作 - 我必须明确删除声明如果我希望编译器能够为我生成这些操作,则可以从 private:
部分中提取这些操作。
即使在 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<=>
的一部分。
编译器不知道您是想要指针比较还是深度(内部)比较。
不实现它并让程序员自己做会更安全。然后他们可以做出他们喜欢的所有假设。
operator=
)通常在与比较运算符相同的上下文中工作 - 也就是说,期望在执行 a = b
之后,a == b
为真。编译器使用与 operator=
相同的聚合值语义来提供默认 operator==
绝对是有意义的。我怀疑 paercebal 在这里实际上是正确的,因为 operator=
(和复制 ctor)仅为 C 兼容性而提供,他们不想让情况变得更糟。
恕我直言,没有“好”的理由。之所以有这么多人同意这个设计决定,是因为他们没有学会掌握基于值的语义的力量。人们需要编写大量自定义复制构造函数、比较运算符和析构函数,因为他们在实现中使用原始指针。
当使用适当的智能指针(如 std::shared_ptr)时,默认的复制构造函数通常很好,假设的默认比较运算符的明显实现也很好。
答案是 C++ 没有做 == 因为 C 没有,这就是为什么 C 只提供默认 = 而首先不提供 == 的原因。 C 想要保持简单:C 实现 = by memcpy;但是,由于填充,== 无法由 memcmp 实现。因为填充没有初始化,所以 memcmp 说它们是不同的,即使它们是相同的。空类也存在同样的问题:memcmp 说它们是不同的,因为空类的大小不为零。从上面可以看出,实现 == 比在 C 中实现 = 更复杂。一些代码example与此相关。如果我错了,感谢您的指正。
operator=
使用 memcpy - 这仅适用于 POD 类型,但 C++ 也为非 POD 类型提供了默认的 operator=
。
在此video中,STL 的创建者 Alex Stepanov 大约在 13:00 解决了这个问题。总而言之,在观察了 C++ 的演变之后,他认为:
不幸的是 == 和 != 没有被隐式声明(并且 Bjarne 同意他的观点)。一个正确的语言应该为你准备好这些东西(他进一步建议你不应该定义一个破坏 == 语义的!=)
这种情况的原因在 C 中有其根源(与许多 C++ 问题一样)。在那里,赋值运算符是通过逐位赋值隐式定义的,但这不适用于 ==。可以在 Bjarne Stroustrup 的这篇文章中找到更详细的解释。
在后续问题中,为什么不使用成员之间的比较,他说了一件令人惊奇的事情:C 是一种本土语言,为 Ritchie 实现这些东西的人告诉他,他发现这很难实现!
然后他说在(遥远的)未来 == 和 != 将被隐式生成。
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 操作的示例,因为没有合理的默认方式来使用 x
和 y
坐标对两个点进行排序......
std::set
来确保所有点都是唯一的,而 std::set
仅使用 operator<
。
auto
:对于这种情况,我们是否可以始终假设它是来自 #include <compare>
的 std::strong_ordering
?
std::common_comparison_category_t
,对于此类,它成为默认排序 (std::strong_ordering
)。
无法定义默认 ==
,但您可以通过通常应自己定义的 ==
定义默认 !=
。为此,您应该执行以下操作:
#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 有效并且一直有效。
C++0x 有 对默认函数提出了建议,因此您可以说 default operator==;
我们了解到,明确这些内容会有所帮助。
operator==
。这是一个遗憾。
从概念上讲,定义平等并不容易。即使对于 POD 数据,有人可能会争辩说,即使字段相同,但它是不同的对象(在不同的地址),它也不一定相等。这实际上取决于运营商的使用情况。不幸的是,您的编译器不是通灵的,无法推断。
除此之外,默认功能是在脚下射击自己的绝佳方式。您描述的默认值基本上是为了保持与 POD 结构的兼容性。然而,它们确实造成了足够多的破坏,开发人员忘记了它们,或者默认实现的语义。
int
等于创建它的那个;对于两个 int
字段中的 struct
,唯一合乎逻辑的做法是以完全相同的方式工作。
+
运算符提出几乎相同的论点,因为它与浮点数无关;即 (x + y) + z
!= x + (y + z)
,由于 FP 舍入的方式。 (可以说,这是一个比 ==
更糟糕的问题,因为它适用于普通数值。)您可能建议添加一个适用于所有数字类型(甚至 int)的新加法运算符,并且几乎与 {1 完全相同} 但它是关联的(不知何故)。但是,如果没有真正帮助那么多人,你就会给语言增加臃肿和混乱。
只是为了让这个问题的答案随着时间的推移而保持完整:从 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
适用于所有比较运算符,并由编译器完全优化掉。
这有充分的理由吗?为什么执行逐个成员的比较会成为问题?
这在功能上可能不是问题,但在性能方面,默认的逐个成员比较可能比默认的逐个成员分配/复制更不理想。与分配顺序不同,比较顺序会影响性能,因为第一个不相等的成员意味着可以跳过其余的成员。因此,如果有一些通常相等的成员,您想最后比较它们,编译器不知道哪些成员更有可能相等。
考虑这个示例,其中 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
}
}
(当然,如果编译器认为它们没有副作用,则它有权忽略比较的顺序,但大概它仍然会从源代码中获取它自己没有更好信息的地方。)
我同意,对于 POD 类型类,编译器可以为您完成。但是,您可能认为简单的编译器可能会出错。所以还是让程序员来做比较好。
我确实有一个 POD 案例,其中两个字段是唯一的 - 所以比较永远不会被认为是正确的。然而,我只需要在有效负载上进行比较——编译器永远无法理解或自己无法弄清楚的东西。
此外 - 他们不会花很长时间来写他们吗?!
==
运算符忽略比较 POD 类的三个成员变量之一引起的运行时错误更有趣的了:/
Foo(const Foo&) = delete; // no copy constructor
和Foo& Foo=(const Foo&) = delete; // no assignment operator
struct
的这些行为,但我确实希望它让class
表现不同(并且理智地)。在此过程中,除了默认访问权限之外,它还会在struct
和class
之间提供更有意义的区别。