C++'s std::sort Strict Weak Ordering Trap: The Comparator That Crashes on Ties

2026-07-15

This function sorts employees by salary, highest first. It compiled cleanly, passed all unit tests with distinct salaries, and shipped to production. Then payroll ran a quarterly report on 50,000 rows where hundreds of employees shared the same salary band. The service segfaulted.

#include <algorithm>
#include <vector>
#include <string>

struct Employee {
    std::string name;
    int salary;
};

// Sort employees by salary, highest first.
void sortBySalary(std::vector<Employee>& employees) {
    std::sort(employees.begin(), employees.end(),
        [](const Employee& a, const Employee& b) {
            return a.salary >= b.salary;  // highest first
        });
}

int main() {
    std::vector<Employee> team;
    for (int i = 0; i < 10000; ++i)
        team.push_back({"emp" + std::to_string(i), 50000});
    sortBySalary(team);  // may crash, hang, or corrupt memory
}

The Bug

The comparator uses >= instead of >. That single character violates C++'s strict weak ordering requirement, and the standard says the resulting behavior is undefined. Not "arbitrary order" — undefined. Real implementations of std::sort have crashed, looped forever, or written past the end of the input.

The requirement is subtle. A comparator comp(a, b) must be irreflexive: comp(a, a) must be false. With >=, comparing any element to itself returns true, claiming an element is strictly ordered before itself. It also violates antisymmetry: when a.salary == b.salary, both comp(a, b) and comp(b, a) return true, telling the algorithm that a < b and b < a simultaneously.

Introsort — libstdc++'s std::sort — uses this predicate to decide partition boundaries. When the predicate lies, elements can be swapped past the partition sentinel, and the quicksort inner loop's pointer arithmetic walks off the end of the array. On small inputs with distinct keys the bug hides. On large inputs with duplicates it detonates, because introsort's quickselect pivots keep landing on equal-valued runs and each recursive step compounds the corruption.

The fix is one character:

void sortBySalary(std::vector<Employee>& employees) {
    std::sort(employees.begin(), employees.end(),
        [](const Employee& a, const Employee& b) {
            return a.salary > b.salary;  // strict >, not >=
        });
}

With >, equal salaries make the comparator return false in both directions, which correctly encodes "these are equivalent under this ordering." std::sort is then free to leave equivalent elements in any order (it isn't stable — use std::stable_sort if you need insertion order preserved among ties).

The same trap lurks in every language that lets you pass a Boolean less-than predicate: Rust's sort_by expects an Ordering, which sidesteps this. But Python's functools.cmp_to_key, Java's Comparator, and JavaScript's Array.sort all inherit it — a comparator that returns 0 for "equal" but returns non-zero for compare(x, x) will silently produce nonsense output in Python and Java, and in C++ will corrupt memory.

How to catch it: compile with libc++ or libstdc++ in debug mode (-D_GLIBCXX_DEBUG). Both ship comparator validators that call your predicate with equal arguments and abort if it returns true. Run this in CI on realistic data — not the tiny distinct-value fixtures that let this bug ship.

Key Takeaway: A sort comparator must return false when its arguments are equal — >= and <= violate strict weak ordering and hand std::sort a license to corrupt memory.

All newsletters