How to Define Comparison Operators by Default in C++
Implementing comparison operators in C++ is easier said than done.
Indeed, for most types, if we could talk to the compiler we would say something like: “to order them, use a lexicographical order on their members”.
But when it comes to writing the corresponding code, things get more complicated.
However, a classical technique using std::tuple
makes the code much more concise for comparison operators, and it should be used by default. (At least before C++20, as C++20 made comparison operators even easier to write).
Let’s see the technique involving std::tuple
and then how the comparison operators situation evolves with C++20.
A naive implementation
Before C++20 and without using std::tuple
, the code for operator<
can be complicated.
To illustrate consider the following class:
struct MyType { int member1; std::string member2; std::vector<double> member3; int member4; double member5; };
Writing operator<
by hand could look like this:
bool operator<(MyType const& lhs, MyType const& rhs) { if (lhs.member1 < rhs.member1) return true; if (rhs.member1 < lhs.member1) return false; if (lhs.member2 < rhs.member2) return true; if (rhs.member2 < lhs.member2) return false; if (lhs.member3 < rhs.member3) return true; if (rhs.member3 < lhs.member3) return false; if (lhs.member4 < rhs.member4) return true; if (rhs.member4 < lhs.member4) return false; return lhs.member5 < rhs.member5; }
This code is more complicated than it should. Indeed, the intention of the programmer is to “do the natural thing”, which means for operator<
a lexicographical comparison. But this code doesn’t say it explicitly.
Instead, it invites the reader to inspect it, run it in their head, formulate the hypothesis that it is a lexicographical comparison, and run it again in their head to make sure. Not really expressive code.
Moreover, this code is dangerous. A typo can easily slip in and cause a bug. And in practice, this happens! I’ve fixed bugs like this several times. One of them took me some time to diagnose, as its effect was to make the std::sort
algorithm crash, only on certain platforms. Nice.
Even before C++20, there is a more expressive and safer way to write comparison operators.
Compare your type like a std::tuple
We want lexicographical comparison on the members of the class. One way to achieve this is to reuse some existing code in the standard library that already implements lexicographical comparison: the comparison of std::tuples
.
Indeed, std::tuple
have comparison operators, and they implement lexicographical comparisons. We can therefore put all the members of the type into a tuple, and use the comparison operators of std::tuple
.
But we wouldn’t like to make copies of each member of the type into a tuple each time we compare two objects. Instead, we can make a tuple of references to the members and compare them, which avoid copies and keeps the advantage of reusing the code of std::tuple
.
To create a std::tuple
of references, we can use std::tie
. Here is the resulting code:
bool operator<(MyType const& lhs, MyType const& rhs) { return std::tie(lhs.member1, lhs.member2, lhs.member3, lhs.member4, lhs.member5) < std::tie(rhs.member1, rhs.member2, rhs.member3, rhs.member4, rhs.member5); }
This code is more concise, safer and more expressive than the previous implementation: it says that the members are compared like a tuple compares its elements, which means in lexicographical order.
That said, one need to know std::tie
to understand this code. But std::tie
is a common component of the standard library, and is part of the common vocabulary of C++ developers.
For a more advanced technique that implements all comparison operators with this technique with little additional code, check out How to Emulate the Spaceship Operator Before C++20 with CRTP.
In C++20
In C++20, the implementation of operator<
becomes even more concise, safe and expressive:
struct MyType { int member1; std::string member2; std::vector<double> member3; int member4; double member5; friend bool operator<(MyType const& lhs, MyType const& rhs) = default; };
With = default
, we just say to the compile: “do the right thing”. However, this is not how we should define operators by default in C++20. A better way is to use the spaceship operator:
struct MyType { int member1; std::string member2; std::vector<double> member3; int member4; double member5; friend bool operator<=>(MyType const& lhs, MyType const& rhs) = default; };
This way, not only do we get operator<
, but also we get operator==
, operator!=
, operator>
, operator<=
, operator>=
and operator<=>
with their implementations by default.
Each version of C++ brings its lot of features to make our code expressive. But before the newer versions arrive, we can still try to write simple code with the features we have at our disposal.
You will also like
- How to Emulate the Spaceship Operator Before C++20 with CRTP
- Compiler-generated Functions, Rule of Three and Rule of Five
- The Rule of Zero in C++
- The Surprising Limitations of C++ Ranges Beyond Trivial Cases
- A Concise Implementation of Fizzbuzz with std::optional
Share this post!