ChatGPT解决这个技术问题 Extra ChatGPT

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

I am a big fan of letting the compiler do as much work for you as possible. When writing a simple class the compiler can give you the following for 'free':

A default (empty) constructor

A copy constructor

A destructor

An assignment operator (operator=)

But it cannot seem to give you any comparison operators - such as operator== or operator!=. For example:

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

Is there a good reason for this? Why would performing a member-by-member comparison be a problem? Obviously if the class allocates memory then you'd want to be careful, but for a simple class surely the compiler could do this for you?

Of course, also the destructor is provided for free.
In one of his recent talks, Alex Stepanov pointed out that it was a mistake not to have a default automatic ==, in the same way that there is a default automatic assignment (=) under certain conditions. (The argument about pointers is inconsistent because the logic applies both for = and ==, and not just for the second).
@becko, it is one of the first in either the series "Efficient programming with components" or "Programming Conversations" both at A9, available in Youtube.
See this answer for C++20 information: stackoverflow.com/a/50345359

p
pooya13

The argument that if the compiler can provide a default copy constructor, it should be able to provide a similar default operator==() makes a certain amount of sense. I think that the reason for the decision not to provide a compiler-generated default for this operator can be guessed by what Stroustrup said about the default copy constructor in "The Design and Evolution of C++" (Section 11.4.1 - Control of Copying):

I personally consider it unfortunate that copy operations are defined by default and I prohibit copying of objects of many of my classes. However, C++ inherited its default assignment and copy constructors from C, and they are frequently used.

So instead of "why doesn't C++ have a default operator==()?", the question should have been "why does C++ have a default assignment and copy constructor?", with the answer being those items were included reluctantly by Stroustrup for backwards compatibility with C (probably the cause of most of C++'s warts, but also probably the primary reason for C++'s popularity).

For my own purposes, in my IDE the snippet I use for new classes contains declarations for a private assignment operator and copy constructor so that when I gen up a new class I get no default assignment and copy operations - I have to explicitly remove the declaration of those operations from the private: section if I want the compiler to be able to generate them for me.


Good answer. I'd just like to point out that in C++11, rather than making the assignment operator and copy constructor private, you can remove them completely like this: Foo(const Foo&) = delete; // no copy constructor and Foo& Foo=(const Foo&) = delete; // no assignment operator
"However, C++ inherited its default assignment and copy constructors from C" That does not imply why you have to make ALL C++ types this way. They should have just restricted this to plain old PODs, just the types that are in C already, no more.
I can certainly understand why C++ inherited these behaviors for struct, but I do wish that it let class behave differently (and sanely). In the process, it also would have given a more meaningful difference between struct and class beside default access.
S
Sergei Krivonos

Even in C++20, the compiler still won't implicitly generate operator== for you

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

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

But you will gain the ability to explicitly default == 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;
};

Defaulting == does member-wise == (in the same way that the default copy constructor does member-wise copy construction). The new rules also provide the expected relationship between == and !=. For instance, with the declaration above, I can write both:

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

This specific feature (defaulting operator== and symmetry between == and !=) comes from one proposal that was part of the broader language feature that is operator<=>.


@dcmm88 Uhfortunately it won't be available in C++17. I've updated the answer.
A modified proposal that allows the same thing (except the short form) is going to be in C++20 though :)
@artin It makes sense as adding new features to language should not break existing implementation. Adding new library standards or new things compiler can do is one thing. Adding new member functions where they did not exist previously is whole different story. To secure your project from mistakes it would require much more effort. I'd personally prefer compiler flag to switch between explicit and implicit default. You build project from older C++ standard, use explicit default by compiler flag. You already update compiler so you should configure it properly. For new projects make it implicit.
M
Mark Ingram

The compiler wouldn't know whether you wanted a pointer comparison or a deep (internal) comparison.

It's safer to just not implement it and let the programmer do that themselves. Then they can make all the assumptions they like.


That problem doesn't stop it from generating a copy ctor, where it's quite harmful.
Copy constructors (and operator=) generally work in the same context as comparison operators - that is, there is an expectation that after you perform a = b, a == b is true. It definitely makes sense for the compiler to provide a default operator== using the same aggregate value semantics as it does for operator=. I suspect paercebal is actually correct here in that operator= (and copy ctor) are provided solely for C compatibility, and they didn't want to make situation any worse.
-1. Of course you want a deep comparison, if the programmer wanted a pointer comparison, he'd write (&f1 == &f2)
Viktor, I suggest you re-think your response. If the class Foo contains a Bar*, then how would the compiler know whether Foo::operator== wants to compare the address of Bar*, or the contents of Bar?
@Mark: If it contains a pointer, comparing the pointer values is reasonable - if it contains a value, comparing the values is reasonable. In exceptional circumstances, the programmer could override. This is just like the language implements comparison between ints and pointer-to-ints.
a
alexk7

IMHO, there is no "good" reason. The reason there are so many people that agree with this design decision is because they did not learn to master the power of value-based semantics. People need to write a lot of custom copy constructor, comparison operators and destructors because they use raw pointers in their implementation.

When using appropriate smart pointers (like std::shared_ptr), the default copy constructor is usually fine and the obvious implementation of the hypothetical default comparison operator would be as fine.


R
Rio Wing

It's answered C++ didn't do == because C didn't, and here is why C provides only default = but no == at first place. C wanted to keep it simple: C implemented = by memcpy; however, == cannot be implemented by memcmp due to padding. Because padding is not initialized, memcmp says they are different even though they are the same. The same problem exists for empty class: memcmp says they are different because size of empty classes are not zero. It can be seen from above that implementing == is more complicated than implementing = in C. Some code example regarding this. Your correction is appreciated if I'm wrong.


C++ doesn't use memcpy for operator= - that would only work for POD types, but C++ provides a default operator= for non POD types too.
Yeah, C++ implemented = in a more sophisticated way. It seems C just implemented = with a simple memcpy.
T
Triskeldeian

In this video Alex Stepanov, the creator of STL addresses this very question at about 13:00. To summarize, having watched the evolution of C++ he argues that:

It's unfortunate that == and != are not implicitly declared (and Bjarne agrees with him). A correct language should have those things ready for you (he goes further on to suggest you should not be able to define a != that breaks the semantics of ==)

The reason this is the case has its roots (as many of C++ problems) in C. There, the assignment operator is implicitly defined with bit by bit assignment but that wouldn't work for ==. A more detailed explanation can be found in this article from Bjarne Stroustrup.

In the follow up question Why then wasn't a member by member comparison used he says an amazing thing : C was kind of a homegrown language and the guy implementing these stuff for Ritchie told him he found this to be hard to implement!

He then says that in the (distant) future == and != will be implicitly generated.


B
Barry

C++20 provides a way to easily implement a default comparison operator.

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

I'm surprised that they used Point as an example for an ordering operation, since there is no reasonable default way to order two points with x and y coordinates...
@pipe If you don't care in which order the elements are, using default operator makes sense. For example, you might use std::set to make sure all points are unique, and std::set uses operator< only.
About return type auto: For this case can we always assume it will be std::strong_ordering from #include <compare>?
@kevinarpe The return type is std::common_comparison_category_t, which for this class becomes the default ordering (std::strong_ordering).
N
Nate Kohl

It is not possible to define default ==, but you can define default != via == which you usually should define yourselves. For this you should do following things:

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

class FooClass
{
public:
  bool operator== (const FooClass& other) const {
  // ...
  }
};

You can see http://www.cplusplus.com/reference/std/utility/rel_ops/ for details.

In addition if you define operator< , operators for <=, >, >= can be deduced from it when using std::rel_ops.

But you should be careful when you use std::rel_ops because comparison operators can be deduced for the types you are not expected for.

More preferred way to deduce related operator from basic one is to use boost::operators.

The approach used in boost is better because it define the usage of operator for the class you only want, not for all classes in scope.

You can also generate "+" from "+=", - from "-=", etc... (see full list here)


There's a reason rel_ops was deprecated in C++20: because it doesn't work, at least not everywhere, and certainly not consistently. There is no reliable way to get sort_decreasing() to compile. On the other hand, Boost.Operators works and has always worked.
M
MSalters

C++0x has had a proposal for default functions, so you could say default operator==; We've learnt that it helps to make these things explicit.


Move constructor can also be defaulted, but I don't think this applies to operator==. Which is a pity.
P
Paul de Vrieze

Conceptually it is not easy to define equality. Even for POD data, one could argue that even if the fields are the same, but it is a different object (at a different address) it is not necessarily equal. This actually depends on the usage of the operator. Unfortunately your compiler is not psychic and cannot infer that.

Besides this, default functions are excellent ways to shoot oneself in the foot. The defaults you describe are basically there to keep compatibility with POD structs. They do however cause more than enough havoc with developers forgetting about them, or the semantics of the default implementations.


There is no ambiguity for POD structs - they should behave in exact same way any other POD type does, which is value equality (rather then reference equality). One int created via copy ctor from another is equal to the one from which it was created; the only logical thing to do for a struct of two int fields is to work in exact same way.
@mgiuca: I can see considerable usefulness for a universal equivalence relation that would allow any type that behaves as a value to be used as a key in a dictionary or similar collection. Such collections cannot behave usefully without a guaranteed-reflexive equivalence relation, however. IMHO, the best solution would be to define a new operator which all built-in types could implement sensibly, and define some new pointer types which were like the existing ones except that some would define equality as reference equivalence while others would chain to the target's equivalence operator.
@supercat By analogy, you could make almost the same argument for the + operator in that it is non-associative for floats; that is (x + y) + z != x + (y + z), due to the way FP rounding occurs. (Arguably, this is a far worse problem than == because it is true for normal numeric values.) You might suggest adding a new addition operator that works for all numeric types (even int) and is almost exactly the same as + but it is associative (somehow). But then you would be adding bloat and confusion to the language without really helping that many people.
@mgiuca: Having things which are quite similar except at edge cases is often extremely useful, and misguided efforts to avoid such things result in much needless complexity. If client code will sometimes need edge cases to be handled one way, and sometimes need them to be handled another, having a method for each style of handling will eliminate a lot of edge-case-handling code in the client. As for your analogy, there is no way to define operation on fixed-sized floating-point values to yield transitive results in all cases (though some 1980s languages had better semantics...
...than today's in that regard) and thus the fact that they don't do the impossible should not be a surprise. There is no fundamental obstacle, however, to implementing an equivalence relation which would be universally applicable to any type of value which can be copied.
c
cosurgi

Just so that the answers to this question remains complete as the time passes by: since C++20 it can be automatically generated with command auto operator<=>(const foo&) const = default;

It will generate all the operators: ==, !=, <, <=, >, and >=, see https://en.cppreference.com/w/cpp/language/default_comparisons for details.

Due to operator's look <=>, it is called a spaceship operator. Also see Why do we need the spaceship <=> operator in C++?.

EDIT: also in C++11 a pretty neat substitute for that is available with std::tie see https://en.cppreference.com/w/cpp/utility/tuple/tie for a complete code example with bool operator<(…). The interesting part, changed to work with == is:

#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 works with all comparison operators, and is completely optimized away by the compiler.


M
Museful

Is there a good reason for this? Why would performing a member-by-member comparison be a problem?

It may not be a problem functionally, but in terms of performance, default member-by-member comparison is liable to be more sub-optimal than default member-by-member assignment/copying. Unlike order of assignment, order of comparison impacts performance because the first unequal member implies the rest can be skipped. So if there are some members that are usually equal you want to compare them last, and the compiler doesn't know which members are more likely to be equal.

Consider this example, where verboseDescription is a long string selected from a relatively small set of possible weather descriptions.

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

(Of course the compiler would be entitled to disregard the order of comparisons if it recognizes that they have no side-effects, but presumably it would still take its que from the source code where it doesn't have better information of its own.)


But nobody keeps you from writing an optimizing user-defined comparison if you find a performance problem. In my experience that would be a minuscule minority of cases though.
g
graham.reeds

I agree, for POD type classes then the compiler could do it for you. However what you might consider simple the compiler might get wrong. So it is better to let the programmer do it.

I did have a POD case once where two of the fields were unique - so a comparison would never be considered true. However the comparison I needed only ever compared on the payload - something the compiler would never understand or could ever figure out on it's own.

Besides - they don't take long to write do they?!


It's not that they take time to write, it's that it's easy to mess them up (or forget to update them later as you add more member-variables to the class). Nothing is more fun than spending several hours tracking down a run-time bug that was caused by the == operator neglecting to compare one of the three dozen member-variables of a POD class :/