Comparison Operators, including Three-Way Comparison(C++20)
There are equality comparisons==
and !=
,and relational comparisons<
, >
, <=
, >=
.C++20 has also introduced the three-way comparison operator <=>
.
Operator | Meaning and Notes (Old) | Meaning and Notes (C++20) |
---|---|---|
x == y | true if x and y are equalsatisfies EqualityComparable (used by std::unordered_map ) | (x <=> y) == 0 (usually implemented directly, not delegated to three-way unless = default )satisfies std::equality_comparable |
x != y | !(x == y) | !(x == y) |
x < y | true if x is lower than y satisfies LessThanComparable (used by std::set , std::sort , etc.but requires strict weak ordering) | (x <=> y) < 0 may satisfy std::strict_weak_ordering when wrapped in a functor (e.g. std::ranges::less ) |
x > y | y < x | (x <=> y) > 0 |
x <= y | !(x < y) for strong orderings,x == y || x < y otherwise | (x <=> y) <= 0 |
x >= y | y <= x | (x <=> y) >= 0 |
x <=> y | N/A | three-way comparison aka. "spaceship operator" satisfies std::three_way_comparable |
Guidelines
- Comparison operators shouldn't be member functions.1)
- If you define
==
, define!=
too (unless it is rewritten in C++20). - If you define
<
, define>
,<=
, and>=
too. - (C++20) Prefer defining
<=>
over defining each relational operator. - (C++20) Prefer defaulting operators over implementing manually.
- Equality and relational comparisons should match, meaning that
x == y
should be equivalent to!(x < y) && !(y < x)
2) - Don't define
==
in terms of<
, even when you could 3)
1)Otherwise, implicit conversions would be asymmetrical, and ==
is expected to apply the same kinds of implicit conversions to both sides.
2)This equivalence does not apply to float
, but does apply to int
and other strongly ordered types.
3)This is motivated by readability, correctness, and performance.
Implementation and Common Idioms Prior to C++20
Disclaimer |
---|
If you're using C++20, the implementations in this section have been obsoleted. Skip ahead to the C++20 parts unless you're interested in a historical perspective. |
All operators are typically implemented as non-member functions, possibly as hidden friends (friend
s where the function is defined inside the class).All following code examples use hidden friends because this becomes necessary if you need to compare private members anyway.
struct S { int x, y, z; // (In)equality comparison: // implementing a member-wise equality friend bool operator==(const S& l, const S& r) { return l.x == r.x && l.y == r.y && l.z == r.z; } friend bool operator!=(const S& l, const S& r) { return !(l == r); } // Relational comparisons: // implementing a lexicographical comparison which induces a // strict weak ordering. friend bool operator<(const S& l, const S& r) { if (l.x < r.x) return true; // notice how all sub-comparisons if (r.x < l.x) return false; // are implemented in terms of < if (l.y < r.y) return true; if (r.y < l.y) return false; // also see below for a possibly simpler return l.z < r.z; // implementation } friend bool operator>(const S& l, const S& r) { return r < l; } friend bool operator<=(const S& l, const S& r) { return !(r < l); } friend bool operator>=(const S& l, const S& r) { return !(l < r); }};
Note: in C++11, all of these can typically be noexcept
and constexpr
.
Implementing all relational comparisons in terms of <
is not valid if we have a partially ordered member (e.g. float
).In that case, <=
and >=
must be written differently.
friend bool operator<=(const S& l, const S& r) { return l == r || l < r; }friend bool operator>=(const S& l, const S& r) { return r <= l; }
Further Notes on operator<
The implementation of operator<
is not so simple because a proper lexicographical comparison cannot simply compare each member once.{1, 2} < {3, 0}
should be true, even though 2 < 0
is false.
A lexicographical comparison is a simple way of implementing a strict weak ordering, which is needed for containers like std::set
and algorithms like std::sort
. In short, a strict weak ordering should behave like the <
operator for integers, except that some integers are allowed to be equivalent (e.g. for all even integers, x < y
is false).
If x != y
is equivalent to x < y || y < x
, a simpler approach is possible:
friend bool operator<(const S& l, const S& r) { if (l.x != r.x) return l.x < r.x; if (l.y != r.y) return l.y < r.y; return l.z < r.z;}
Common Idioms
For multiple members, you can use std::tie
to implement comparison lexicographically:
#include <tuple>struct S { int x, y, z; friend bool operator<(const S& l, const S& r) { return std::tie(l.x, l.y, l.z) < std::tie(r.x, r.y, r.z); }};
Use std::lexicographical_compare
for array members.
Some people use macros or the curiously recurring template pattern (CRTP) to save the boilerplate of delegating !=
, >
, >=
, and <=
, or to imitate C++20's three-way comparison.
It is also possible to use std::rel_ops
(deprecated in C++20) to delegate !=
, >
, <=
, and >=
to <
and ==
for all types in some scope.
Default Comparisons (C++20)
A substantial amount of comparison operators simply compare each member of a class.If so, the implementation is pure boilerplate and we can let the compiler do it all:
struct S { int x, y, z; // ==, !=, <, >, <=, >= are all defined. // constexpr and noexcept are inferred automatically. friend auto operator<=>(const S&, const S&) = default;};
Note: defaulted comparison operators need to be friend
s of the class, and the easiest way to accomplish that is by defining them as defaulted inside the class. This makes them "hidden friends".
Alternatively, we can default individual comparison operators.This is useful if we want to define equality comparison, or only relational comparison:
friend bool operator==(const S&, const S&) = default; // inside S
See the cppreference article on default comparison.
Expression Rewriting (C++20)
In C++20, if a comparison isn't directly implemented, the compiler will also try to use rewrite candidates.Thanks to this, even if <=>
isn't defaulted (which would implement all operators), we only have to implement ==
and <=>
, and all other comparisons are rewritten in terms of these two.
Operator | Potential Rewrites |
---|---|
x == y | y == x |
x != y | !(x == y) or !(y == x) if equality comparison returns bool |
x < y | (x <=> y) < 0 or 0 < (y <=> x) if comparison result is comparable to zero |
x > y | (x <=> y) > 0 or 0 > (y <=> x) if ... |
x <= y | (x <=> y) <= 0 or 0 <= (y <=> x) if ... |
x >= y | (x <=> y) >= 0 or 0 >= (y <=> x) if ... |
struct S { int x, y, z; // ==, != friend constexpr bool operator==(const S& l, const S& r) noexcept { /* ... */ } // <=>, <, >, <=, >= friend constexpr auto operator<=>(const S& l, const S& r) noexcept { /* ... */ }};
Note: constexpr
and noexcept
are optional, but can almost always be applied to comparison operators.
Three-Way Comparison Operator (C++20)
Note: it is colloquially called "spaceship operator". See also spaceship-operator.
The basic idea behind x <=> y
is that the result tells us whether x
is lower than, greater than, equivalent to, or unordered with y
.This is similar to functions like strcmp
in C.
// old C styleint compare(int x, int y) { if (x < y) return -1; if (x > y) return 1; return 0; // or simply return (x > y) - (x < y);}// C++20 style: this is what <=> does for int.auto compare_cxx20(int x, int y) { if (x < y) return std::strong_ordering::less; if (x > y) return std::strong_ordering::greater; return std::strong_ordering::equal;}// This is what <=> does for float.auto compare_cxx20(float x, float y) { if (x < y) return std::partial_ordering::less; if (x > y) return std::partial_ordering::greater; if (x == y) return std::partial_ordering::equivalent; return std::partial_ordering::unordered; // NaN}
Comparison Categories
The result of this operator is neither bool
nor int
, but a value of comparison category.
Comparison Category | Example | Possible Values |
---|---|---|
std::strong_ordering | int | less , equal = equivalent , greater |
std::weak_ordering | user-defined1) | less , equivalent , greater |
std::partial_ordering | float | less , equivalent , greater , unordered |
std::strong_ordering
s can be converted to std::weak_ordering
, which can be converted to std::partial_ordering
.Values of these categories are comparable to (e.g. (x <=> y) == 0
) and this has similar meaning to the compare
function above.However, std::partial_ordering::unordered
returns false for all comparisons.
1)There are no fundamental types for which x <=> y
results in std::weak_ordering
. Strong and weak orderings are interchangeable in practice; see Practical meaning of std::strong_ordering and std::weak_ordering.
Manual Implementation of Three-Way Comparison
Three-way comparison is often defaulted, but could be implemented manually like:
#include <compare> // necessary, even if we don't use std::is_eqstruct S { int x, y, z; // This implementation is the same as what the compiler would do // if we defaulted <=> with = default; friend constexpr auto operator<=>(const S& l, const S& r) noexcept { // C++17 if statement with declaration makes this more readable. // !std::is_eq(c) is not the same as std::is_neq(c); it is also true // for std::partial_order::unordered. if (auto c = l.x <=> r.x; !std::is_eq(c)) /* 1) */ return c; if (auto c = l.y <=> r.y; !std::is_eq(c)) return c; return l.y <=> r.y; } // == is not automatically defined in terms of <=>. friend constexpr bool operator==(const S&, const S&) = default;};
If all members of S
weren't the same type, then we could either specify the category explicitly (in the return type), or we could obtain it with std::common_comparison_category
:
std::common_comparison_category_t<decltype(l.x <=> l.x), /* ... */>
1)Helper functions like std::is_neq
compare the result of <=>
to zero.They express intent more clearly, but you don't have to use them.
Common Idioms
Alternatively, we can let std::tie
figure out the details:
#include <tuple>struct S { int x, y, z; friend constexpr auto operator<=>(const S& l, const S& r) noexcept { return std::tie(l.x, l.y, l.z) <=> std::tie(r.x, r.y, r.z); }};
Use std::lexicographical_compare_three_way
for array members.