Comparing Custom Types Custom Comparisons Comparing
Comparison operators
==
, !=
, <
, <=
, >
, >=
and <=>
can be overloaded for custom types using operator functions:
Equality: Does A have the same value as B?
bool operator == (…) // equal
bool operator != (…) // not equal
Ordering: Does A go before/after B?
bool operator < (…) // smaller
bool operator <= (…) // smaller equal
bool operator > (…) // greater
bool operator >= (…) // greater equal
3-Way Comparison C++20
auto operator <=> (…)
Comparison functions are not automatically generated by the compiler:
Principles
Value-Based = compares (member) values
C++ | a == b |
---|---|
Java | a.equals(b) |
Identity-Based = compares memory addresses
C++ | &a == &b
// false |
---|---|
Java | a == b |
Reminder: C++ uses Value Semantics
= variables refer to objects themselves, i.e., they are not just references/pointers
This is the default behavior for fundamental types (int
, double
, etc.)
in almost all programming languages and also the default for user-defined types in C++:
- deep copying: produces a new, independent object; object (member) values are copied
- deep assignment: makes value of target equal to that of source object
- deep ownership: member variables refer to objects with same lifetime as containing object
- value-based comparison: variables compare equal/less/… if their values are equal/less/…
Two objects a
and b
are
equal, | if a == b
is true, (their values are the same) |
equivalent, | if !(a < b) && !(b < a)
is true (neither one is ordered before the other) |
incomparable, | if a < b , b < a and a == b
are all false (unordered) |
Partial Relation: incomparable values are allowed
Total Relation: at least one of a < b
, b < a
and a == b
must be true for any pair of values
- container items: string characters, vector elements, …
- day, month & year of a 'Date' type
- numerator & denominator of a 'Fraction' type …
Non-salient members should not take part in comparisons:
- memory management details: vector capacity, buffer addresses, …
- execution details: thread handles, mutexes, …
- value caches: intermediate results, lookup tables, … …
i.e., if a
is equal/equivalent to b
,
then f(a)
is also equal/equivalent to f(b)
as long as function f
reads only comparison-salient state.
std::string a = "123";
std::string b = "123"; // same value as a
int fa = std::stoi(a); // string → int
int fb = std::stoi(b);
if (a == b) { /* then we expect fa == fb */ }
Equality
Does 'a
' have the same value as 'b
'?
bool operator == (T const& a, T const& b);
bool operator != (T const& a, T const& b);
Manual C++98-17
#include <iostream>
class irange {
int min_; int max_;
public: explicit constexpr
irange (int min, int max) noexcept: min_{min}, max_{max} {
if (min_ > max_) std::swap(min_, max_);
}
int min () const noexcept { return min_; }
int max () const noexcept { return max_; }
friend bool operator == (irange const& l, irange const& r) noexcept {
return l.min() == r.min() && l.max() == r.max();
}
friend bool operator != (irange const& l, irange const& r) noexcept {
return !(l == r); // reuse operator==
}
};
int main () {
irange r1 { 0,10};
irange r2 {20,30};
if (r1 == r2) std::cout << "equal\n";
if (r1 != r2) std::cout << "unequal\n";
}
int main () {
irange r1 { 0,10};
irange r2 {20,30};
if (r1 == r2) { … } // false
if (r1 != r2) { … } // true
}
Defaulted C++20
#include <iostream>
class irange {
int min_; int max_;
public: explicit constexpr
irange (int min, int max) noexcept: min_{min}, max_{max} {
if (min_ > max_) std::swap(min_, max_);
}
int min () const noexcept { return min_; }
int max () const noexcept { return max_; }
bool operator == (irange const&) const = default;
};
int main () {
irange r1 { 0,10};
irange r2 {20,30};
if (r1 == r2) std::cout << "equal\n";
if (r1 != r2) std::cout << "unequal\n";
}
- compiler generates recursive comparison of all members
- no need to define
operator!=
, compiler will rewrite calla != b
as!(a == b)
if necessary - we can just use a member function declaration that only takes one right hand side parameter
- requires that every member is
==
comparable
You should generally avoid comparing different types for equality.
However, in rare cases it can be justified, e.g., when two different types represent values of the same underlying domain, e.g., two different integer types representing values in the same range.
Goal: each type should work on both sides of operator
A a;
if (a == a) { … }
B b;
if (a == b) { … }
if (b == a) { … }
class B { … };
class A { public: …
// symmetric
friend bool operator == (A const& a1, A const& a2) noexcept { … }
// asymmetric
friend bool operator == (A const& a, B const& b) noexcept { … }
friend bool operator == (B const& a, A const& b) noexcept { … }
…
};
class B { … };
class A { public: …
// symmetric
bool operator == (A const& a) const noexcept { … } // or = default;
// asymmetric - either one member:
bool operator == (B const& b) const noexcept { … }
// or one free-standing function:
friend bool operator == (A const& a, B const& b) noexcept { … }
…
};
compiler rewrites a call a == b
as b == a
if necessary
Only, if your type has a clearly defined value
like, e.g.,
- math numbers: integer, complex, dual, quaternion, …
- container elements: vector items, string characters, …
- physical quantities: mass, distance, …
- geometric entities: point, rotation, direction, …
- settings/configuration storage
no value:
- input/output stream
- execution primitives: thread, mutex, lock, …
Only, if it models true equality
- Reflexivity:
a == a
must be true - Symmetry: results of
a == b
andb == a
must be the same - Transitivity: if
a == b
andb == c
are both true thena == c
must be true
Do not ignore part of an object's salient state in operator==
:
- case-insensitive string comparison (ignores letter case)
- order of magnitude float comparison (ignores mantissa)
- punctuation-ignoring text comparison
Provide weaker comparisons as descriptively named comparison functions:
compare_case_insensitive
same_order_of_magnitude
same_excluding_punctuation
Compare the members that form the actual value
- container items: string characters, vector elements, …
- day, month & year of a 'Date' type
- numerator & denominator of a 'Fraction' type
Non-salient members should not take part in comparisons:
- memory management details: vector capacity, buffer addresses, …
- execution details: thread handles, mutexes, …
- value caches: intermediate results, lookup tables, …
Copies must always compare equal:
std::string a = "xyz";
std::string b = a; // copy of a
if (a == b) { /* then OK */ }
Salient member changed to different value ⇒ object must no longer compare equal to its previous state:
std::string a = "xyz";
std::string b = "xyz"; // same value as a
a[1] = "W"; // <--
if (a != b) { /* then OK */ }
Equality should imply substitutability
If a
is equal to b
,
then f(a)
should also be equal to f(b)
as long as function f
reads only comparison-salient state.
std::string a = "123";
std::string b = "123"; // same value as a
int fa = std::stoi(a); // string → int
int fb = std::stoi(b);
if (a == b) { /* then we expect fa == fb */ }
Start comparison with members that are most likely to differ
Employ logic operator short-circuiting or early returns
- compare size (= number of elements) of a container (vector,string,map,set,…) before comparing all individual elements
- compare house numbers and streets before comparing cities of addresses
- compare first names of persons before comparing last names
- could use cheap hash comparison (e.g. using a bloom filter) before comparing all data
Also provide a matching operator!=
C++98-17
// implement it in terms of operator==
friend bool operator != (A const& l, A const& r) {
return !(l == r);
}
C++20 only needs operator ==
because compiler rewrites call a != b
as !(a == b)
Make operator==
noexcept
Comparison operations should never encounter exceptional situations:
Comparisons must always be read-only operations
- no out-of-memory errors
- no file access errors …
No problem, if (part) of a value is not available (e.g., internal memory buffer not yet allocated, file not opened, …)
- both values not available ⇒ result:
equal
- one value available, one not available ⇒ result:
not equal
- both values available ⇒ compare values
- both values not available ⇒ result:
Ordering
Does 'a
' go before or after 'b
'?
bool operator < (T const& a, T const& b);
bool operator <= (T const& a, T const& b);
bool operator > (T const& a, T const& b);
bool operator >= (T const& a, T const& b);
Example: Comparable Date Example: Date Example
#include <iostream>
enum class month { jan = 1, feb = 2, mar = 3, apr = 4, may = 5, jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11…, dec = 12 };
struct date {
int yyyy; month mm; int dd;
friend bool operator < (date const& l, date const& r) noexcept {
if (l.yyyy < r.yyyy) return true;
if (l.yyyy > r.yyyy) return false;
if (l.mm < r.mm) return true;
if (l.mm > r.mm) return false;
if (l.dd < r.dd) return true;
return false;
}
// more ordering operators
// friend bool operator <= ( … ) { … }
// friend bool operator > ( … ) { … }
// friend bool operator >= ( … ) { … }
// equality comparison operators
// friend bool operator == ( … ) { … }
// friend bool operator != ( … ) { … }
};
int main () {
date earlier {2017, month::dec, 24};
date later {2018, month::may, 12};
if (earlier < later) { std::cout << "less\n"; … }; // true
}
You should generally avoid comparisons between different types.
However, in rare cases it can be justified, e.g., when two different types represent values of the same underlying domain, e.g., two different integer types representing values in the same range.
Goal: each type should work on both sides of operator
A a;
if (a < a) { … }
B b;
if (a < b) { … }
if (b < a) { … }
class B { … };
class A { public: …
// symmetric
friend bool operator < (A const& a1, A const& a2) noexcept { … }
// asymmetric
friend bool operator < (A const& a, B const& b) noexcept { … }
friend bool operator < (B const& a, A const& b) noexcept { … }
…
};
class B { … };
class A { public: …
// symmetric
bool operator < (A const& a) const noexcept { … } // or = default;
// asymmetric - either one member:
bool operator < (B const& b) const noexcept { … }
// or one free-standing function:
friend bool operator < (A const& a, B const& b) noexcept { … }
…
};
compiler rewrites a call a < b
as b < a
if necessary
Prefer to implement just C++20's operator <=>
instead of
individual ordering operators.
C++20 Prefer operator<=>
and use comparison categories!
(see
next section).
Only if their meaning is clear and unambiguous!
Example: We should not provide an ordering for a type that models
an interval
(like
irange
)!
What could it mean that interval A is less than
, i.e., should be
ordered before interval B?
- only the left bound of A could be smaller than the left bound of B,
- both bounds of A could be smaller than the left bound of B,
- or A could have a smaller width (max-min) than B.
- ambiguous interface
- code that uses such an ordering would be confusing and error-prone
Don't provide an ordering just because a (standard) algorithm requires one!
Pass a custom comparator (as lambda or function) instead!
struct Box {
int id;
double weight;
Location origin;
Location target;
};
auto heaviest (std::vector<Box> const& v) {
return max_element(begin(v), end(v),
[](Box const& a, Box const& b){ return a.weight < b.weight; });
}
You should either provide all or none of the 4 ordering operators
Everyone – including yourself – expects that if
a < b
compiles, then b > a
does also compile.
Incomplete implementation of ordering operators will almost certainly lead to countless situations where you or users of your type have to waste time on diagnosing compiler errors.
Providing only strict comparisons <
and >
but not
<=
and >=
can be justified in very rare situations,
but only if you don't provide ==
and !=
as well.
If you provide (all) ordering operators you should also provide
equality comparison with ==
and !=
- if
a <= b
compiles, thena < b || a == b
should also compile - if
a >= b
compiles, thena > b || a == b
should also compile
The equality induced by <=
and >=
should be identical to that of ==
a <= b
anda < b || a == b
should give the same answera >= b
anda > b || a == b
should give the same answer
Make comparison operators noexcept
Comparison operations should never encounter exceptional situations:
Comparisons must always be read-only operations
- no out-of-memory errors
- no file access errors …
No problem, if (part) of a value is not available (e.g., internal memory buffer not yet allocated, file not opened, …)
- both values not available ⇒ result:
equal
- one value available, one not available ⇒ result:
not equal
- both values available ⇒ compare values
- both values not available ⇒ result:
3-Way Comparisons C++20 3-Way C++20 3-Way C++20
(a <=> b) < 0 |
if a < b |
(a <=> b) > 0 |
if a > b |
(a <=> b) == 0 |
if a and b are equal/equivalent |
The return value comes from one of three comparison categories:
category | equivalent values | incomparable values |
---|---|---|
std::strong_ordering |
are indistinguishable (truly equal) | forbidden |
std::weak_ordering |
can be distinguished | forbidden |
std::partial_ordering |
can be distinguished | allowed |
Integers (
char
,int
, …) andbool
4 <=> 6
→strong_ordering::less
5 <=> 5
→strong_ordering::equal
8 <=> 1
→strong_ordering::greater
Floating-Point Types
are partially ordered, because of incomparable "Not A Number" values for representing division-by-zero, overflow, etc. (e.g., IEEE 754 silent+signalling NaNs)
4.1 <=> 6.3
→partial_ordering::less
5.2 <=> 5.2
→partial_ordering::equivalent
8.3 <=> 1.4
→partial_ordering::greater
Orderable standard types (
vector
,string
, …)"ab"s <=> "bc"s
→strong_ordering::less
"ab"s <=> "ab"s
→strong_ordering::equal
"bc"s <=> "ab"s
→strong_ordering::greater
If one of the boolean comparison operators
==
, !=
, <
, <=
, >
or >=
is not found,
the compiler will rewrite the call expression in terms of <=>
if necessary:
a == b
can be rewritten as(a <=> b) == 0
a != b
can be rewritten as(a <=> b) != 0
a < b
can be rewritten as(a <=> b) < 0
a > b
can be rewritten as(a <=> b) > 0
a <= b
can be rewritten as(a <=> b) <= 0
a >= b
can be rewritten as(a <=> b) >= 0
Implementing operator<=>
C++20
#include <compare>
#include <iostream>
enum class month { jan = 1, feb = 2, mar = 3, apr = 4, may = 5, jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11…, dec = 12 };
struct date {
int yyyy; month mm; int dd;
auto operator <=> (date const&) const = default;
};
int main () {
date earlier {2017, month::dec, 24};
date later {2018, month::may, 12};
if (earlier < later) std::cout << "less\n";
if (earlier > later) std::cout << "greater\n";
auto cmp = (earlier <=> later);
if (cmp < 0) std::cout << "less\n";
if (cmp == 0) std::cout << "equal\n";
if (cmp > 0) std::cout << "geater\n";
}
The default operator <=>
recursively compares
all data members in order of declaration.
int main () {
date earlier {2017, month::dec, 24};
date later {2018, month::may, 12};
if (earlier < later) { … } // true
if (earlier > later) { … } // false
auto cmp = (earlier <=> later);
if (cmp < 0) { … } // true
if (cmp == 0) { … } // false
if (cmp > 0) { … } // false
}
#include <compare>
#include <iostream>
enum class month { jan = 1, feb = 2, mar = 3, apr = 4, may = 5, jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11…, dec = 12 };
struct date {
int yyyy; month mm; int dd;
auto operator <=> (date const& o) const noexcept {
// use built-in <=>; for ints & enums
if (auto cmp = yyyy <=> o.yyyy; cmp != 0) { return cmp; }
if (auto cmp = mm <=> o.mm; cmp != 0) { return cmp; }
return dd <=> o.dd;
}
};
int main () {
date earlier {2017, month::dec, 24};
date later {2018, month::may, 12};
if (earlier < later) std::cout << "less\n";
if (earlier > later) std::cout << "greater\n";
auto cmp = (earlier <=> later);
if (cmp < 0) std::cout << "less\n";
if (cmp == 0) std::cout << "equal\n";
if (cmp > 0) std::cout << "geater\n";
}
default <=>
only
If one only requests a compiler-generated default implementation of
operator <=>
then the compiler can rewrite expressions
involving all other comparison operators in terms of <=>
.
#include <iostream>
#include <iomanip>
struct X {
int x = 0;
auto operator <=> (X const&) const = default;
};
int main () {
X a {1};
X b {3};
std::cout << std::boolalpha;
std::cout << (a == b) << '\n'; // (a <=> b) == 0
std::cout << (a != b) << '\n'; // (a <=> b) != 0
std::cout << (a < b) << '\n'; // (a <=> b) < 0
std::cout << (a <= b) << '\n'; // (a <=> b) <= 0
std::cout << (a > b) << '\n'; // (a <=> b) > 0
std::cout << (a >= b) << '\n'; // (a <=> b) >= 0
}
custom <=>
only
If one provides a manual implementation of operator <=>
then
only the ordering operators <
, <=
, >
, >=
can be automatically rewritten in terms of <=>
.
#include <iostream>
#include <iomanip>
struct X {
int x = 0;
auto operator <=> (X const& other) const {
return (x <=> other.x);
}
};
int main () {
X a {1};
X b {3};
std::cout << std::boolalpha;
// std::cout << (a == b) << '\n';
// std::cout << (a != b) << '\n';
std::cout << (a < b) << '\n'; // (a <=> b) < 0
std::cout << (a <= b) << '\n'; // (a <=> b) <= 0
std::cout << (a > b) << '\n'; // (a <=> b) > 0
std::cout << (a >= b) << '\n'; // (a <=> b) >= 0
}
custom <=>
+ ==
The compiler is allowed to automatically rewrite
- the ordering operators
<
,<=
,>
,>=
in terms of the manually implementedoperator <=>
operator !=
in terms ofoperator ==
(either manually implemented or automaticall generated)
#include <iostream>
#include <iomanip>
struct X {
int x = 0;
auto operator <=> (X const& other) const {
return (x <=> other.x);
}
bool operator == (X const& other) const = default;
};
int main () {
X a {1};
X b {3};
std::cout << std::boolalpha;
std::cout << (a == b) << '\n';
std::cout << (a != b) << '\n'; // !(a == b)
std::cout << (a < b) << '\n'; // (a <=> b) < 0
std::cout << (a <= b) << '\n'; // (a <=> b) <= 0
std::cout << (a > b) << '\n'; // (a <=> b) > 0
std::cout << (a >= b) << '\n'; // (a <=> b) >= 0
}
Rewriting
The compiler is allowed to do the following expression rewrites, if an implementation (defaulted or custom) for a comparison operator is not available.
Equality | Ordering | |
---|---|---|
Primary | == | <=> |
Secondary | != | < , > , <= , >= |
Expressions involving secondary comparison operators can be rewritten in terms of the associated primary operator.
The argument order of primary operators can be reversed, i.e.,
if a.operator==(b)
is not available, but b.operator==(a)
is,
the expression a == b
can be rewritten as b == a
.
Category | Possible Values | |
---|---|---|
| :: | less, greater, equivalent, equal |
| :: | less, greater, equivalent |
| :: | less, greater, equivalent, unordered |
less | a before b |
greater | a after b |
equivalent | a neither before nor after b |
equal | a is the same as b |
unordered | a not comparable with b |
class A {
M m_;
public: …
std::strong_ordering
operator <=> (A const& rhs) const noexcept {
if (m_ == rhs.m_) return strong_ordering::equal;
if (m_ < rhs.m_) return strong_ordering::less;
return strong_ordering::greater;
}
};
Examples
- case-insensitive string comparison ('aBc' and 'ABC' are equivalent, but can be distinguished)
- order of magnitude float comparison (30 and 50 are equivalent, but can be distinguished)
std::weak_ordering compare_case_insensitive (std::string const&, std::string const&) noexcept { … }
std::weak_ordering compare_order_of_magnitude (float, float) noexcept { … }
Examples
- number types with an incomparable "Not A Number" value
(for representing division-by-zero, overflow, etc.),
e.g., IEEE 754 NaNs (may be used for
float
,double
,long double
) - tree node adjacency:
child
less
than parent, parentgreater
than child, equivalent = same node, unordered = nodes not adjacent
std::partial_ordering compare_adjacency (TreeNode const&, TreeNode const&) noexcept { … }
Only if their meaning is clear and unambiguous!
Example: We should not provide an ordering for a type that models
an interval
(like
irange
)!
What could it mean that interval A is less than
, i.e., should be
ordered before interval B?
- only the left bound of A could be smaller than the left bound of B,
- both bounds of A could be smaller than the left bound of B,
- or A could have a smaller width (max-min) than B.
- ambiguous interface
- code that uses such an ordering would be confusing and error-prone
Don't provide an ordering just because a (standard) algorithm requires one!
Pass a custom comparator (as lambda or function) instead!
struct Box {
int id;
double weight;
Location origin;
Location target;
};
auto heaviest (std::vector<Box> const& v) {
return max_element(begin(v), end(v),
[](Box const& a, Box const& b){ return a.weight < b.weight; });
}
Avoid weak and partial orderings as result of operator<=>
Use a named comparison function instead:
std::weak_ordering compare_case_insensitive (std::string const&, std::string const&) noexcept { … }
std::weak_ordering compare_order_of_magnitude (float, float) noexcept { … }
std::partial_ordering compare_adjacency (TreeNode const&, TreeNode const&) noexcept { … }
The equality induced by <=>
(and thus <=
and >=
)
should be identical to that of ==
(a <=> b) <= 0
anda < b || a == b
should give the same answer(a <=> b) >= 0
anda > b || a == b
should give the same answera <= b
anda < b || a == b
should give the same answera >= b
anda > b || a == b
should give the same answer
Make all comparison functions noexcept
Comparison operations should never encounter exceptional situations:
Comparisons must always be read-only operations
- no out-of-memory errors
- no file access errors …
No problem, if (part) of a value is not available (e.g., internal memory buffer not yet allocated, file not opened, …)
- both values not available ⇒ result:
equal
- one value available, one not available ⇒ result:
not equal
- both values available ⇒ compare values
- both values not available ⇒ result:
Comments…