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) {
    // ...
  }

Heterogeneous and friend

The comparison above is homogeneous, 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 heterogeneous 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 heterogeneous 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.