C++ comparison (first attempt)
The first attempt in C++ to handle equality and order: ask programmer to explicitly implement all comparisons operators.
This article is part of a series on the history of regular data type in C++.
Comparison operator
C++ allowed customizing comparison operators.
For example for equality you can do:
1
2
3
4
5
6
7
8
9
10
11
class foo {
// ...
public:
bool operator==(const foo & other) const {
// compare *this with other
// return true if equal
// return false otherwise
}
// ...
};
and it will be called when you compare using ==
two variables of type foo
:
1
2
3
4
5
6
// assuming a and b are of type foo
// operator == is called here
// *this is a, other is b
if (a == b) {
// ...
}
Heterogenous and friend
The comparison above is homogenous, the types compared are the same type. It
compares foo
with foo
. Most of the time you want to compare apples with
apples and oranges with oranges.
But the case where the types are different also occurs. This is a heterogenous
comparison. For example we want to compare std::string
with a literal string:
1
2
3
4
5
6
7
8
9
10
11
class foo {
// ...
public:
bool operator==(const char * other) {
// compare *this with other
// return true if equal
// return false otherwise
}
// ...
};
But the rules of the language were such that it looked for a comparison member function only for the type on the left hand side of the operation:
1
2
3
4
5
6
7
8
9
10
11
// assuming a is a foo
// operator == is called here
// *this is foo, other is a string literal
// it works
if (a == b) {
// ...
}
// but this does not work
if (b == a) {
// ...
}
So the language added the ability to make such comparison functions standalone functions which can accept arguments of different types:
1
2
3
4
// outside the foo class
bool operator==(const foo & x, const char * y) {
// compare x with y
}
But then how do you ensure that such a function can access private members of
foo
? Ah, then you make them friend and either you continue to define them
outside the class or you define them inside the class as below (but they are
not member functions, they don’t take this
as a hidden additional argument):
1
2
3
4
5
6
7
8
9
10
11
class foo {
// ...
public:
friend bool operator==(const foo & x, const char * y) {
// compare x with y
// return true if equal
// return false otherwise
}
// ...
};
Unnecessary duplication
There are six such comparison operators:
==
for equal to!=
for “not equal”<
for less than>
for greater than<=
for less than or equal to>=
for greater than or equal to
But they are not really independent.
Unfortunately the language required you to define them explicitly one by one,
so for example if ==
is defined, but !=
is not defined, then the following
fails at compile time:
1
2
3
4
// fails if == is defined, but != is not defined
if (a != b) {
// ...
}
This can be fixed by defining !=
as well.
But other than syntax, the reality is that semantically comparisons are not really independent from each other, in particular they appear in groups.
So the practice is to define one of the functions in the group, while the others in the group are implemented in terms of the basic one.
In the equality group !=
is implemented in terms of ==
:
1
2
3
4
5
6
7
bool operator==(const foo & x, const foo & y) {
// actual implementation of ==
}
bool operator!=(const foo & x, const foo & y) {
return !(x == y);
}
Similar for the order group, the other comparisons are implemented in terms of
<
:
1
2
3
4
5
6
7
8
9
10
11
12
13
bool operator<(const foo & x, const foo & y) {
// actual implementation of <
}
bool operator<=(const foo & x, const foo & y) {
return !(y < x);
}
bool operator>(const foo & x, const foo & y) {
return y < x;
}
bool operator>=(const foo & x, const foo & y) {
return !(x < y);
}
Similar issues apply to the heterogenous operations, shown here for equality:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool operator==(const foo & x, const char * y) {
// actual implementation of ==
}
bool operator!=(const foo & x, const char * y) {
return !(x == y);
}
bool operator==(const char * x, const foo & y) {
return y == x;
}
bool operator!=(const char * x, const foo & y) {
return y != x;
}
To avoid such duplication, the standard attempted to help with the
std::rel_ops
namespace, but not very successfully, it has been deprecated in
C++20, so will not explore std::rel_ops
further.
lhs and rhs vs x and y
In terms of variable names, traditionally rhs
and lhs
were used to stand
for “right hand side” and “left hand side”, though here I borrow from Alex
Stepanov the more intuitive notation x
and y
.