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

13

363

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 and move constructor
  • A destructor
  • Assignment operators (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?

Publea answered 20/10, 2008 at 9:42 Comment(5)
Of course, also the destructor is provided for free.Dark
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).Johannesburg
@becko, it is one of the first in either the series "Efficient programming with components" or "Programming Conversations" both at A9, available in Youtube.Johannesburg
See this answer for C++20 information: stackoverflow.com/a/50345359Bo
My list of reasons - https://mcmap.net/q/93706/-no-operator-found-while-comparing-structs-in-c-duplicateGreyso
L
82

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.

Lassie answered 20/10, 2008 at 9:53 Comment(15)
That problem doesn't stop it from generating a copy ctor, where it's quite harmful.Decani
Copy constructors are used in an entirely different context than comparison operators. And, IMHO, its context is clear in what it is doing.Alcinia
There is the problem of compatibility with C: C89 produces code for structs that mimicks C++'s assignement (and probably copy constructors... I have to check). Thus, it is normal C++ generates similar codes.Betweentimes
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.Snooperscope
-1. Of course you want a deep comparison, if the programmer wanted a pointer comparison, he'd write (&f1 == &f2)Beta
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?Lassie
@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.Amalgamate
-1 as pointed out by others this is not consistent argument when comparing to operator=Cleat
None of the stated reasons are valid. That would not be the craziest thing in the c++ standard if it said that compilers must provide a default 'operator==' comparing each field value. And hell, if we want a security against pointers, just issue a warning or an error in such cases.Winer
@MarkIngram: your remark makes no sense. the compiler implemented operator == should just call operators == on all members if they are accessible. so for example a clone_ptr would not break the deep chain. neither would std::vector. however pointers and smart pointers would compare shallowly.Schappe
The compiler wouldn't need to figure that out. One of the two choices could be specified as the standard, then the compiler vendors would just have to follow that standard. The obvious choice, of course, would be shallow comparison for pointers, and value comparison for members, exactly the way v.oddou describes.Hilversum
Why not applying same logic for the assignment operator too? There is an inconsistency issue.Coquina
@JollyRoger, not really, assignment operator can just assign each member variable, there's no ambiguity.Lassie
I don't see how it's a problem to compare the value of each member discretely. If the user wanted to compare the addresses of each object, then they'd implement &A == &B and be explicit about it. As for deep comparisons like strcmp, they don't cause an issue because C++ is rather clear about pointers being integer values (addresses) which can be compared normally (==) with another pointer.Ferminafermion
This inconsistency still continues within the standart committee as well. In C++ 20, they changed their mind. Now you can request a default comparison operator (under strong ordering condition).Pulsar
U
348

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.

Upholster answered 20/10, 2008 at 14:53 Comment(3)
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 operatorIdioglossia
"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.Revolutionize
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.Petronius
M
134

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

Maze answered 8/1, 2015 at 10:23 Comment(3)
@dcmm88 Uhfortunately it won't be available in C++17. I've updated the answer.Maze
A modified proposal that allows the same thing (except the short form) is going to be in C++20 though :)Korykorzybski
@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.Johnajohnath
L
82

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.

Lassie answered 20/10, 2008 at 9:53 Comment(15)
That problem doesn't stop it from generating a copy ctor, where it's quite harmful.Decani
Copy constructors are used in an entirely different context than comparison operators. And, IMHO, its context is clear in what it is doing.Alcinia
There is the problem of compatibility with C: C89 produces code for structs that mimicks C++'s assignement (and probably copy constructors... I have to check). Thus, it is normal C++ generates similar codes.Betweentimes
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.Snooperscope
-1. Of course you want a deep comparison, if the programmer wanted a pointer comparison, he'd write (&f1 == &f2)Beta
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?Lassie
@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.Amalgamate
-1 as pointed out by others this is not consistent argument when comparing to operator=Cleat
None of the stated reasons are valid. That would not be the craziest thing in the c++ standard if it said that compilers must provide a default 'operator==' comparing each field value. And hell, if we want a security against pointers, just issue a warning or an error in such cases.Winer
@MarkIngram: your remark makes no sense. the compiler implemented operator == should just call operators == on all members if they are accessible. so for example a clone_ptr would not break the deep chain. neither would std::vector. however pointers and smart pointers would compare shallowly.Schappe
The compiler wouldn't need to figure that out. One of the two choices could be specified as the standard, then the compiler vendors would just have to follow that standard. The obvious choice, of course, would be shallow comparison for pointers, and value comparison for members, exactly the way v.oddou describes.Hilversum
Why not applying same logic for the assignment operator too? There is an inconsistency issue.Coquina
@JollyRoger, not really, assignment operator can just assign each member variable, there's no ambiguity.Lassie
I don't see how it's a problem to compare the value of each member discretely. If the user wanted to compare the addresses of each object, then they'd implement &A == &B and be explicit about it. As for deep comparisons like strcmp, they don't cause an issue because C++ is rather clear about pointers being integer values (addresses) which can be compared normally (==) with another pointer.Ferminafermion
This inconsistency still continues within the standart committee as well. In C++ 20, they changed their mind. Now you can request a default comparison operator (under strong ordering condition).Pulsar
A
48

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.

Ancillary answered 3/4, 2009 at 17:5 Comment(1)
The other answers try to justify the lack of an automatically defined operator== by comparing with C or give workarounds but this is the real answer It is just bad design. Comparison should actually recursively cascade by default.Foison
G
45

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.

Geisel answered 7/12, 2011 at 16:50 Comment(2)
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.Mazzola
Yeah, C++ implemented = in a more sophisticated way. It seems C just implemented = with a simple memcpy.Geisel
P
35

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.

Philippines answered 27/4, 2014 at 21:15 Comment(1)
I hereby declare this to be the real accepted answer.Guberniya
B
25

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<=>
Bo answered 15/5, 2018 at 8:11 Comment(4)
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...Bohunk
@Bohunk 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.Bo
About return type auto: For this case can we always assume it will be std::strong_ordering from #include <compare>?Headman
@Headman The return type is std::common_comparison_category_t, which for this class becomes the default ordering (std::strong_ordering).Bo
T
16

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)

Thanh answered 20/10, 2008 at 11:23 Comment(1)
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.Aragonite
D
11

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.

Decani answered 20/10, 2008 at 9:59 Comment(1)
Move constructor can also be defaulted, but I don't think this applies to operator==. Which is a pity.Snooperscope
B
5

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.

Brunette answered 20/10, 2008 at 10:6 Comment(14)
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.Snooperscope
Well, but what if the POD struct has a value whose relevance is dependent on another value. In the case that the value is not relevant, it's no longer bit-for-bit equality. Equality can really range between one and the same object (only with pointers/references) to anything somewhat the save (perhaps of a subclass)Brunette
That's true but C++ does have a clear definition of a "value" (it's whatever is copied in the default copy constructor). The default equality, if there is one, would certainly work the same way: it would simply call operator= on each field. This would ensure that any POD values are compared as values, pointers are compared by reference, and objects with overridden operator= methods are compared as the class designer intended. I'd say conceptually it is easy to define equality. They didn't do it for other reasons.Diastasis
@PavelMinaev Even for POD structs, it depends on what they represent on whether they are the same. Should equality be defined as identity, equivalence, or an other option. I'll agree with you that for many POD structs, equivalence (the same data values) is the right answer.Brunette
@PavelMinaev: All of the uses I can think of for a general-purpose means of equivalence testing would require that every object compare equivalent to itself. Unfortunately, IEEE-754 forbids the == and != operators from exhibiting such behavior.Bosanquet
@mgiuca: Given struct {double v;} a,b; a.v=0.0/0.0; b=a;, what should a==b report? Were it not for the broken behavior of == with floating-point types, then a.v==b.v would be true, and a==b would pose no problem. As it is, though, while I can see great value in having a standard means of asking objects to test equivalence, such usefulness would be predicated upon a contractual requirement that the test behave as an equivalence relation, something the == operator can't do with all types.Bosanquet
@Bosanquet Why do you assume the automatic == operator should be required to satisfy reflexitivity? I agree that would be ideal, but given C++ already has == operators that aren't reflexive (as you point out), an automatically generated == operator for a composite type can only be as good as the == operators of its fields. This doesn't detract from the usefulness of automatically generating a default == operator that says "we're equal if all our fields are equal according to ==".Diastasis
@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.Bosanquet
@supercat: That is really just an argument for == implementations to obey the rules of equivalence relations. Unfortunately, that ship has sailed, and I don't think introducing a new operator that's almost exactly the same as == except in a few corner cases is really that helpful. As far as I know, all built-in types obey equivalence laws other than float and double. Of course, user-defined types can define == and violate those laws, but so they can too with your new operator. None of this affects whether having automatic == for user-defined types is useful or not.Diastasis
@mgiuca: There are many situations where it's necessary to test floating-point numbers for equivalence. If programmers wanting to test fp equivalence have to write something like bool equals(double x, double y) { return (x==y) || (x!=x) && (y!=y); }, such testing is apt to be inefficient even on hardware which would allow direct equivalence testing. Adding a means of equivalence testing which would work for fp types, but could also be implemented by other types that promise that it represents an equivalence relation would seem nicer than adding one just for fp types.Bosanquet
@Bosanquet Sorry for slow reply. Yeah, I'm not disagreeing that it would be helpful to have a proper equivalence for FP. Rather, that the root of the issue you are talking about is not that the == operator is broken, but rather that equality for FPs is broken. == for almost all intents and purposes does represent an equivalence class, and therefore extending it automatically to structs would make sense. It just has this one rare edge case where it's broken. Adding a new operator would only confuse matters and solve hardly any problems. ("Why are there 2 equality operators?!")Diastasis
@Bosanquet 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.Diastasis
@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...Bosanquet
...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.Bosanquet
A
5

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.

Animalize answered 6/9, 2019 at 12:29 Comment(0)
C
2

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.)

Capriccio answered 4/12, 2015 at 20:1 Comment(1)
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.Jariah
S
0

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?!

Serena answered 20/10, 2008 at 9:50 Comment(1)
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 :/Villagomez

© 2022 - 2024 — McMap. All rights reserved.