Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A comparator answers one question, under four rules · std::tie compares three keys in one line · Four ways to write one multi-key order, and when each fits · Practise: comparators, keys and ties. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

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 <= b breaks it.
  • Asymmetric: comp(a, b) and comp(b, a) are never both true. a != b breaks 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.

Four groups of teams in a line, ordered by problems solved and penalty; Asha and Ravi tie, and so do Kofi and Lena.5 solved, 180 minAsha, Ravi: a tie5 solved, 210 minMei4 solved, 95 minKofi, Lena: a tie3 solved, 40 minOmarcomes beforecomes beforecomes before

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

  1. Question 1 of 2 For two elements that tie under a comparator, what must comp(a, b) and comp(b, a) return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: false and false

    comp(a, b) returns true when a must come before b, and false otherwise, including when the two tie. Two elements tie, or are equivalent, when neither comes before the other.

  2. Question 2 of 2 Is std::abs(a) < std::abs(b) a strict weak ordering on ints, given that it makes 3 and -3 tie?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes. Ties are allowed, and here they form clean groups, such as {3, -3}, which the sort puts in a line

    A strict weak ordering does not forbid ties: it sorts the elements into groups of ties and puts the groups in a line. The tie groups of the standings, by solved and penalty only, are the same idea.

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 C++ · standings.cpp
// 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);
}
The same order two ways, with both compilers Python · standings_session.py
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:

Interactive · stage by stage How std::tie decides which of two teams comes first Step through three comparisons from standings.cpp. The tuples compare key by key from the left, and the first key that differs decides.
How std::tie decides which of two teams comes first, step 6 of 6. Showing solved, more first (5 and 5: equal), penalty, fewer first (180 < 210: decides), name, A to Z (not compared), the answer (Asha comes first). In focus: penalty, fewer first.the counts differthe minutes differsolved, more first5 and 5: equalpenalty, fewer first180 < 210: decidesname, A to Znot comparedthe answerAsha comes firstthe minutes differ

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 In standings.cpp, the comparator is std::tie(y.solved, x.penalty, x.name) < std::tie(x.solved, y.penalty, y.name). What does putting y.solved on the left do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It reverses that one key, so more solved comes first, while penalty and name stay ascending

    Mei against Kofi was decided at the first key, 4 < 5 with the sides swapped, so the team with more problems solved came first.

  2. Question 2 of 2 The projection version sorts by std::tuple(-t.solved, t.penalty, t.name). For which key type would negating be wrong?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A key that can hold the smallest int, an unsigned type or a string

    Swapping the two sides of a key works for every type; negating works for most signed integers but not for the smallest int, whose negation overflows, nor for unsigned types and strings.

Four ways to write one multi-key order, and when each fits

The same three-key order can be written in four ways:

  • Chained if statements: one comparison per key, returning at the first difference; swap a and b for 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:

Interactive · answer and compare Which way of writing the order fits your sort? Answer the three questions about the order you need. Ways that cannot express it are ruled out with the reason; ways that work with a catch get a caveat.
Does a key need a rule of its own, such as one value always last?
Should the type sort this way everywhere it is compared, in every sort and every std::set?
Is the order one computed key, such as a length or a score?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four ways to write a multi-key order, against descending keys, the easy mistake and where the order lives
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

  1. Question 1 of 2 A struct defaults operator<=>. In which order does it compare the members?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In the order the members are declared

    A defaulted operator<=> makes a type sortable everywhere, with its members compared in declaration order, while the other three ways describe one order at the place where you sort.

  2. Question 2 of 2 You sort a vector of words by length only, at one place in the program. Which of the four ways says that order most directly?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A projection to the length, with the default comparison doing the rest

    Each way is best for something: chained if statements for keys with special rules, std::tie for most contest code, a defaulted operator<=> for types that always sort the same way, and a projection for one computed key.

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.

  1. Question 1 of 9 When must a comparator comp(a, b) given to std::sort return true?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: When a must come before b

    A comparator answers one question: does a come first? For two elements that tie it must return false in both directions, which is why <= is not a comparator.

  2. Question 2 of 9 Why is return a <= b; a broken comparator?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: It says that every value comes before itself, which breaks the first rule

    For a == b, a <= b is true, so the comparator claims that a comes before a. A strict weak ordering never puts an element before itself. The code compiles, and the program has undefined behaviour.

  3. Question 3 of 9 When are two elements a tie under a comparator?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: When comp(a, b) and comp(b, a) are both false

    A tie means neither comes before the other. Two different values can tie, such as two teams with the same solved count and penalty when the comparator ignores names.

  4. Question 4 of 9 A std::set of teams uses a comparator that looks only at problems solved. You insert the six teams of the standings. How many does the set keep?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: Three, one for each solved count, 5, 4 and 3

    Sorted containers use the same rule to decide what a duplicate is. Give a set's comparator enough keys to tell apart everything you mean to keep.

  5. Question 5 of 9 In standings.cpp, why does y.solved stand on the left side of the std::tie comparison?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: Swapping that one key between the two sides sorts it in descending order

    Tuples compare member by member from the left. Putting y.solved where x.solved would be reverses the order of that key only, so more problems solved comes first, while penalty and name stay ascending.

  6. Question 6 of 9 Asha and Ravi both solved 5 problems with 180 minutes. Why do they come out as Asha, Ravi with both compilers, whatever their input order?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: The name is the last key, so it breaks their tie and fixes the order

    The name breaks the last ties, so the order is the same whatever order the teams arrived in. Without it, Asha and Ravi would be one group of ties, in either order.

  7. Question 7 of 9 How do you sort a whole range of ints in descending order with std::ranges::sort?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Pass std::ranges::greater{}

    For a whole range in descending order, pass std::greater<>{} to std::sort, or std::ranges::greater{} to std::ranges::sort.

  8. Question 8 of 9 Which is the easy mistake when the order is written as a projection to a key?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: A key that allocates on every call, such as a new string built for each comparison

    Each way has its easy mistake: a wrong sign in one branch, mixed sides, members in the wrong order, or a key that allocates on every call.

  9. Question 9 of 9 Which algorithm keeps elements that tie in the order they came in?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: std::stable_sort

    Only stable_sort promises that ties keep their input order. std::sort may put them in any order, so code that needs the input order of ties must ask for it.

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.

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

  1. 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.

  2. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A comparator returns true only when a must come before b; never use <=.
  • Custom comparators must be strict weak orderings: irreflexive, asymmetric, transitive, with transitive ties.
  • Use std::tie or a projection for several keys, and swap the sides of a key, or negate it, to sort it descending.
  • Use std::stable_sort when ties must keep their input order.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.