C++ Module 19 – C++ for DSA and competitive programming
Custom comparators and strict weak ordering
Write custom comparators for std::sort that order by several keys, learn the four rules of a strict weak ordering, and see what breaks when one fails.
What you will learn
- Write comparators and projections for multi-key and descending orders
- Explain why a comparator must be a strict weak ordering, and test one for the four rules
- Sort indices or key and index pairs instead of large objects when that is faster
- Choose std::stable_sort when ties must keep their input order
Before you start
What you’ll learn · Before you start
What you will learn
- Write comparators and projections for multi-key and descending orders
- Explain why a comparator must be a strict weak ordering, and test one for the four rules
- Sort indices or key and index pairs instead of large objects when that is faster
- Choose std::stable_sort when ties must keep their input order
Before you start
On this page
Six contest teams sorted by problems solved, then penalty, then name came out in the same order with both compilers,
whatever order they arrived in. A comparator tells std::sort, std::set and the other sorted algorithms and
containers which of two elements comes first: comp(a, b) returns true when a must come before b, and false
otherwise, including when the two tie. Custom comparators let you sort by several keys, by a key you compute, or in
descending order.
They come with one requirement: the comparator must be a strict weak ordering: The rules a comparator of the ordered containers and of std::sort must follow: comp(a, a) is false, comp is transitive, and keys that neither go before the other form groups of equivalent keys. a < b qualifies; a <= b does not. More in “std::map, std::set and custom comparators”. No element comes before itself, two elements never both come first, “comes before” chains from a to b to c, and ties chain the same way. A comparator that breaks a rule is undefined behaviour: What a program does when it breaks a rule for which the C++ standard sets no requirements, such as reading past the end of an array. The compiler may assume it never happens, so the result can change with the compiler and the optimisation level. More in “Object lifetime and undefined behaviour in C++”, and the damage ranges from a wrongly sorted result to a crash. Below, the four rules come first, then one order with three keys, written two ways and compared with two others.
A comparator answers one question, under four rules
The requirement comes down to four properties, as cppreference’s page on the Compare requirement lists them, and each one has a classic way of being broken:
- Irreflexive:
comp(a, a)is false, so nothing comes before itself.a <= bbreaks it. - Asymmetric:
comp(a, b)andcomp(b, a)are never both true.a != bbreaks it, because it says each of two different values comes first. - Transitive: a before b and b before c means a before c. A circular order, like rock, paper, scissors, breaks it.
- Transitive ties: if a ties with b and b with c, a ties with c. “Close enough counts as equal”, with a tolerance, breaks it.
Two elements tie, or are equivalent keys: Two keys a and b are equivalent for a comparator comp when neither comp(a, b) nor comp(b, a) is true. Ordered containers keep one element per group of equivalent keys and find a key by equivalence, never by ==. More in “std::map, std::set and custom comparators”, when neither comes before the other. A strict weak ordering does not forbid ties:
it sorts the elements into groups of ties and puts the groups in a line. std::abs(a) < std::abs(b) is a valid
strict weak ordering in which 3 and -3 tie. The diagram shows the groups for the contest standings below, ordered by
problems solved and penalty minutes only.
Sorted containers use the same rule to decide what a duplicate is. A std::set keeps one element per group of ties,
so a set of teams whose comparator looks only at problems solved would keep a single team for each count and quietly
drop the others. Give a set’s comparator enough keys to tell apart everything you mean to keep.
A strict weak ordering puts the elements into groups of ties, and the groups in a line
Text description of the diagram
The diagram shows the six teams of standings.cpp ordered by problems solved, more first, then by penalty minutes, fewer first, without the name.
The order forms four groups, each coming before the next: Asha and Ravi with 5 solved and 180 minutes; Mei with 5 solved and 210 minutes; Kofi and Lena with 4 solved and 95 minutes; Omar with 3 solved and 40 minutes.
Inside a group the comparator returns false in both directions, so the two teams are a tie, and a sort may put them in either order. Adding the name as a last key breaks every tie, and then every group holds one team.
Checkpoint
Check: What a comparator returns, and its four rules
std::tie compares three keys in one line
Standings order teams by problems solved, more first, then by penalty minutes, fewer first, then by name. The program writes that order twice:
// standings.cpp: contest standings ordered by problems solved (more first), then penalty minutes (fewer first),
// then name (alphabetical), written two ways: with std::tie in a lambda, and with a projection.
#include <algorithm>
#include <cstdio>
#include <string>
#include <tuple>
#include <vector>
struct Team {
std::string name;
int solved;
int penalty;
};
static void print(const char* title, const std::vector<Team>& teams) {
std::printf("%s\n", title);
for (const Team& t : teams) std::printf(" %-8s %d solved, %3d min\n", t.name.c_str(), t.solved, t.penalty);
}
int main() {
const std::vector<Team> input = {
{"Mei", 5, 210}, {"Asha", 5, 180}, {"Kofi", 4, 95}, {"Ravi", 5, 180}, {"Lena", 4, 95}, {"Omar", 3, 40},
};
// A comparator returns true when a must come before b. y.solved on the left makes "more solved" come first.
std::vector<Team> a = input;
std::sort(a.begin(), a.end(), [](const Team& x, const Team& y) {
return std::tie(y.solved, x.penalty, x.name) < std::tie(x.solved, y.penalty, y.name);
});
print("with std::tie:", a);
// The same order as a key: tuples compare member by member, and -solved puts larger counts first.
std::vector<Team> b = input;
std::ranges::sort(b, {}, [](const Team& t) { return std::tuple(-t.solved, t.penalty, t.name); });
print("with a projection:", b);
} from session import run
# The standings with both compilers: the two ways print the same order.
run("""
clang++ -std=c++23 -O2 -Wall -Wextra standings.cpp -o standings && ./standings
g++-16 -std=c++23 -O2 -Wall -Wextra standings.cpp -o standings-gcc && ./standings-gcc
""") Output
$ clang++ -std=c++23 -O2 -Wall -Wextra standings.cpp -o standings && ./standings with std::tie: Asha 5 solved, 180 min Ravi 5 solved, 180 min Mei 5 solved, 210 min Kofi 4 solved, 95 min Lena 4 solved, 95 min Omar 3 solved, 40 min with a projection: Asha 5 solved, 180 min Ravi 5 solved, 180 min Mei 5 solved, 210 min Kofi 4 solved, 95 min Lena 4 solved, 95 min Omar 3 solved, 40 min $ g++-16 -std=c++23 -O2 -Wall -Wextra standings.cpp -o standings-gcc && ./standings-gcc with std::tie: Asha 5 solved, 180 min Ravi 5 solved, 180 min Mei 5 solved, 210 min Kofi 4 solved, 95 min Lena 4 solved, 95 min Omar 3 solved, 40 min with a projection: Asha 5 solved, 180 min Ravi 5 solved, 180 min Mei 5 solved, 210 min Kofi 4 solved, 95 min Lena 4 solved, 95 min Omar 3 solved, 40 min
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 standings_session.py
Both ways printed Asha, Ravi, Mei, Kofi, Lena and Omar with both compilers. std::tie builds tuples of references,
and tuples lexicographic comparison: Comparing two sequences element by element from the first: the first pair of elements that differ decides, and a sequence that runs out first is the smaller. Tuples, pairs, std::array and strings compare this way, like words in a dictionary. More in “std::pair, std::tuple and structured bindings” from the left, so one comparison covers
all three keys. For the descending key,
y.solved stands where x.solved would, which reverses that key alone. The projection: A function that a range algorithm applies to each element before comparing or testing it, such as &Student::marks in std::ranges::sort(students, {}, &Student::marks): the comparison sees two numbers, and the sort moves whole students. More in “C++ range algorithms and projections” version gives
std::ranges::sort a key for each team instead of a comparator, and -t.solved does the reversing; the default
comparison then applies to the keys. The name breaks the last ties, so the order is the same whatever order the
teams arrived in. Step through three of the comparisons the sort makes, with the teams’ own numbers:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Asha against Ravi. Both solved 5, so the first members of the two tuples are equal and the comparison moves on to the next key.
- The penalty ties too. Both took 180 minutes, so the penalties are equal as well. Without a third key the two teams would tie, and std::sort could put them in either order.
- The name decides. "Asha" < "Ravi", so the left tuple is smaller: the comparator returns true, and Asha comes before Ravi whatever order they arrived in.
- Mei against Kofi. Now x is Mei with 5 solved and y is Kofi with 4. The left tuple starts with y.solved, 4, and the right one with x.solved, 5: 4 < 5 decides at once.
- More solved comes first. The comparator returns true, so Mei comes before Kofi. Swapping the sides of the solved key, and only that key, is what makes more solved come first.
- Asha against Mei. With x as Asha and y as Mei, both solved 5, so the penalties decide: 180 < 210, and the team with fewer minutes comes first. The name is never read.
For a whole range in descending order, pass std::greater<>{} to std::sort, or std::ranges::greater{} to
std::ranges::sort. Negating a key works for most signed integers, but not for the smallest int, whose negation
overflows, nor for unsigned types and strings; swapping the two sides of that key works for every type.
Checkpoint
Check: Three keys with std::tie or a projection
Four ways to write one multi-key order, and when each fits
The same three-key order can be written in four ways:
- Chained
ifstatements: one comparison per key, returning at the first difference; swapaandbfor a descending key. The easy mistake is a<where>was meant, in one branch. Best for keys that need special rules. std::tie: two tuples of the keys, compared with<; swap the two sides for a descending key. The easy mistake is mixing the sides of different keys. Best for most contest code.- A defaulted
operator<=>: the members, declared in the order of the keys; a descending key needs a negated or wrapped member. The easy mistake is members in the wrong order. Best for types that always sort the same way. - A projection: a function from an element to its key; negate the key, or pass
std::greater. The easy mistake is a key that allocates on every call. Best for sorting by one computed key.
Answer three questions about your order, and the table rules out the ways that cannot express it:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Way | A descending key | The easy mistake | Where the order lives |
|---|---|---|---|
| chained if statements | Good swap a and b in that branch | Mixed a < where > was meant, in one branch | Mixed at the place where you sort |
| std::tie of the keys | Good swap the two sides of that key | Mixed mixing the sides of different keys | Mixed at the place where you sort |
| a defaulted operator<=> | Poor needs a negated or wrapped member | Mixed members declared in the wrong order | Good in the type: it sorts the same way everywhere |
| a projection to a key | Mixed negate the key, or pass std::greater | Mixed a key that allocates on every call | Mixed at the place where you sort |
How the answers decide
- “Does a key need a rule of its own, such as one value always last?” Yes, a key needs a special rule: rules out a defaulted operator<=>. A defaulted operator<=> compares the members as declared and has no place for a rule of its own.
- “Does a key need a rule of its own, such as one value always last?” Yes, a key needs a special rule: adds a caveat to std::tie of the keys and a projection to a key. They compare each key with its usual order; a special rule needs a key built for it first.
- “Should the type sort this way everywhere it is compared, in every sort and every std::set?” No, only at this one sort: adds a caveat to a defaulted operator<=>. A defaulted operator<=> makes this order the type's own, for every comparison in the program.
- “Should the type sort this way everywhere it is compared, in every sort and every std::set?” Yes, this is the type's own order: adds a caveat to chained if statements, std::tie of the keys and a projection to a key. Each describes the order at one place; every other sort and every set has to repeat it.
- “Is the order one computed key, such as a length or a score?” Yes, one key: adds a caveat to chained if statements and std::tie of the keys. They work, but a projection says the one key directly and lets the default comparison do the rest.
A defaulted operator<=> makes a type sortable everywhere, with its members compared in declaration order, while
the other three describe one order at the place where you sort. Two more tools complete the set:
stable sort: A sort that keeps elements which compare equal in the order they had before it, as std::stable_sort does. std::sort promises nothing about equal elements, so a second sort by another key keeps the first sort's order among ties only when it is stable. More in “Essential C++ algorithms: find, sort, partition” keeps ties in their input order, and sorting a vector of indices
moves small integers instead of large elements.
Checkpoint
Check: Choosing a way to write the order
Practise: comparators, keys and ties
Practise the four rules, the three keys and the four ways; your weakest idea comes first, and each option explains itself.
Practice that adapts to you
Practise this lesson
9 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Comparators that break the rules, reproduced and caught Pro: opens with a Pro, Premium or Ultimate pass
A comparator that breaks strict weak ordering rarely fails on a small test. This part reproduces a “close enough” comparator that sorts silently wrong, shows which library builds catch it, and gives you a short auditor that names the rule a comparator breaks.
In this part:
- A close-enough comparator that sorts silently wrong, reproduced
- Which debug builds catch a broken comparator
- An auditor that names the rule a comparator breaks
- The proof behind the four rules: why ties form ordered groups
5 runnable examples (C++, Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Index sorts, stable ties and five classic orders Pro: opens with a Pro, Premium or Ultimate pass
Real orders have more keys, larger elements and ties that must stay where they are. This part measures when sorting indices pays, shows the two-pass stable sort, and ends with an exercise of five orders whose tests are heavy on ties.
In this part:
- When sorting indices pays, measured on small and large records
- Ties that must keep their order: stable_sort and the two-pass sort
- Exercise: five custom orders, with tests full of ties
- One solution: angles without atan2, intervals that nest, stable ties
7 runnable examples (C header, C header (orders.h), C header (orders_starter.h), C++, Python) · about 9 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
What must a comparator passed to std::sort return, and what does strict weak ordering mean in plain words?
It returns true when its first argument must come before its second, and false otherwise, including when the two tie, so it never returns true for an element against itself. Strict weak ordering is the name for the rules that answer must follow: nothing comes before itself, two elements never both come first, "comes before" chains from a to b to c, and ties chain as well. Ties are allowed: |a| < |b| lets 3 and -3 tie and is still valid, because the ties form clean groups that the sort puts in a line. a <= b breaks the first rule and a tolerance such as b - a > 1.0 breaks the last; with either, std::sort has undefined behaviour.
-
Mid to senior
A pull request sorts sensor readings with return b - a > 0.5; so that "noise does not reorder close readings", and its tests pass. Review it: what can go wrong, how would you show it, and what would you write instead?
-
Mid to senior
Sort 2 × 10^5 points with integer coordinates up to 10^9 in absolute value by polar angle, counter-clockwise from the positive x-axis. Why is atan2 risky here, and how do you compare two points exactly?
-
Fresher to mid
You must sort 10^6 records of 200 bytes by an int key and still find each record by its original position later. Compare sorting the records, sorting indices and sorting (key, index) pairs, with the costs you expect.
Key takeaways
- A comparator returns
trueonly whenamust come beforeb; never use<=. - Custom comparators must be strict weak orderings: irreflexive, asymmetric, transitive, with transitive ties.
- Use
std::tieor a projection for several keys, and swap the sides of a key, or negate it, to sort it descending. - Use
std::stable_sortwhen ties must keep their input order.
References
- Working Draft, Programming Languages: C++ (N4950) (ISO/IEC JTC1/SC22/WG21)
- C++ named requirements: Compare (cppreference.com)
- Algorithms library (cppreference.com)
- The GNU C++ Library Manual: Debug Mode (GNU Project)
- Hardening Modes (libc++ documentation) (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress