Special class operations. Comparing
This article looks at the next layer of fundamental behaviour of a class: how two instances compare (for equality and order).
Syntax
There are 6 comparison operators in C++: ==
, !=
, <
, >
, <=
, >=
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (a == b) // 'operator ==' used here
{
// ...
}
if (a!= b) // 'operator !=' used here
{
// ...
}
if (a < b) // 'operator <' used here
{
// ...
}
//...
The operators, to take ==
for example, return bool
and can be defined as
members for a class (with one argument, in addition to this
) or as a free
function with two arguments and return a bool
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class X
{
// as a member function
bool operator== (const X & other) const noexcept
{
// ...
}
// when as a free function ofen declared as a friend
// to allow it to access private members
friend bool operator== (const X & left, const X & right) noexcept;
};
// OR as a free function
bool operator== (const X & left, const X & right) noexcept
{
// ...
}
You really need to define at most two of them: ==
and <
, the others can be
(and should be, to maintain sanity) implemented in terms of them e.g.:
1
2
3
4
5
6
7
8
9
10
11
12
13
bool operator!= (const X & left, const X & right) noexcept
{
// calls 'operator ==' implementation
// and negates result
return !(left == right);
}
bool operator> (const X & left, const X & right) noexcept
{
// calls 'operator <' implementation
// but with switched positions for left and right
return right < left;
}
Practicalities
Often a simple way to implement comparison operators for a class is to write a
function to tie the member variables together and rely on the tuple
comparison implementation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <string>
#include <tuple>
struct X
{
int i;
std::string s;
auto members() noexcept const
{
// return tuple of const references to member variables
return std::tie(i, s);
}
};
bool operator== (const X & left, const X & right) noexcept
{
// rely on the std::tuple.operator== implementation
return left.members() == right.members();
}
bool operator< (const X & left, const X & right) noexcept
{
// rely on the std::tuple.operator< implementation
return left.members() < right.members();
}
// Implement the rest in terms of the two above
// example below for !=, but similar for >, <=, >=
bool operator!= (const X & left, const X & right) noexcept
{
return !(left == right);
}
#include <iostream>
int main()
{
X one { 42, "one"};
X two { 42, "two" };
std::cout << ((one == two) ? "same" : "different") << std::endl;
std::cout << ((one < two) ? "smaller" : "bigger") << std::endl;
}
// Compile with $ g++ --std=c++1y main.cpp
// Running ./a.out will print:
// different
// smaller
Meaning
On one side we have regular classes. Think of int
. We can construct
it (with a default constructor), we can copy and move it (both construct and
assign), we can compare for equality and less than. And all of these properties
play together as you would expect them to: the order is total (e.g. given
two variables they are either equal, or one is smaller), a copy is equal to the
variable we copied from etc.
On the other side we have classes for which comparison just does not make
sense. See noncopyable
.
And then there are the inbetweeners. This is not an exclusive list but here are some examples.
Pointers (including say shared_ptr
) do a shallow comparison, comparing the
addresses, not the actual pointed data.
float
and double
are almost regular, except for NaN
(all comparisons with
NaN
return false
).
Most basic containers are regular (assuming the value_type
is regular)
though, unlike an int
, copy might throw. When compared, they do what you
would expect, e.g. compare lexicographically.
The unordered containers however only compare for equality, not for less than. The reason is runtime cost.
Another example where the equality is not implementable because of cost are
functions. Two functions that for the identical inputs provide the same result
are equal in mathematical sense, but good luck with implementing a operator==
with this meaning.
Another issue is that sometimes is better to have a comparison function that
returns 3 possible values (less, equal or greater), than the comparison
operators that return a bool
.
The above issues just scratch the surface of the subject, I’ll leave to that for the moment.