为什么C++编译器不定义运算符==和运算符!=?

Why don't C++ compilers define operator== and operator!=?

本文关键字:运算符 定义 C++ 编译器 为什么      更新时间:2023-10-16

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

  • 默认(空)构造函数
  • 复制构造函数
  • 析构函数
  • 赋值运算符(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++没有默认的operator==()?",而不是"为什么C++有默认的赋值和复制构造函数?",答案是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

但您将获得显式默认==的能力,因为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;
};

默认==执行成员级==(与默认复制构造函数执行成员级复制构造的方式相同)。新规则还提供了CCD_ 11和CCD_。例如,使用上面的声明,我可以同时写两个:

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!

这个特定的特性(默认的operator====!=之间的对称性)来自一个提议,该提议是operator<=>这一更广泛的语言特性的一部分。

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

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

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

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

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

在这个视频中,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<=>

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

#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< ,则<=、>、>=的运算符当使用CCD_ 21时可以从中推导出。

但在使用std::rel_ops时应该小心,因为可以为您不期望的类型推导比较运算符。

从基本运算符推导相关运算符的更可取的方法是使用boost::运算符。

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

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

C++0x有一个默认函数的建议,所以您可以说default operator==;我们已经了解到,把这些事情说清楚会有帮助。

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

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

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

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

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

编辑:在C++11中,std::tie也提供了一个非常巧妙的替代品,请参阅https://en.cppreference.com/w/cpp/utility/tuple/tie以获得具有CCD_ 27的完整代码示例。与==一起工作的有趣部分是:

#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
    }
}

(当然,如果编译器认识到比较没有副作用,它有权忽略比较的顺序,但据推测,它仍然会从没有更好信息的源代码中获取que。)

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

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

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