Ordered Comparisons (PartialOrd and Ord)

How Rust overloads the comparison operators through PartialOrd and Ord, the difference between partial and total orderings, and how to implement custom ordering for your own types.

Rust’s comparison operators — <, <=, >, and >= — are not built into the language. They are syntax sugar that the compiler desugars into method calls on the traits PartialOrd and Ord. If you have ever written x < y in Rust, you have used operator overloading, even if you never typed impl. This section covers exactly what those two traits demand, why Rust splits ordering into partial and total, and how to implement both correctly.

The Traits Behind Comparison Operators

Two traits control ordered comparisons. PartialOrd is the one that makes < and friends available. Ord is the stronger version — it says every pair of values can be compared, and the result is always a meaningful Less, Equal, or Greater.

Both live in std::cmp. The inheritance chain is strict:

  • PartialOrd extends PartialEq. A type cannot be ordered unless you can also ask whether two values are equal.
  • Ord extends Eq plus PartialOrd. A type that claims a total order must be fully equality-comparable and must provide a PartialOrd implementation that is consistent with that total order.

In practice this means: if you derive or implement Ord, you must also have PartialEq, Eq, and PartialOrd in place. The compiler will not let you write impl Ord for MyType without those bounds already satisfied.

The Ordering Enum

Every comparison boils down to a value of type std::cmp::Ordering. It is a simple enum with three variants:

pub enum Ordering {
    Less,
    Equal,
    Greater,
}

PartialOrd’s core method returns Option<Ordering>None when the values are incomparable. Ord’s core method returns Ordering directly because an incomparable pair does not exist in a total order.

Ordering has a few useful methods on its own:

  • reverse() returns the opposite ordering (Less becomes Greater, etc.)
  • then(other) chains a secondary comparison that is only consulted when the first is Equal. This is the building block for multi-field lexicographic ordering.
use std::cmp::Ordering;
let by_name = "Alice".cmp("Bob");      // Less
let by_age  = 30.cmp(&25);             // Greater
// Combine: compare by name, then by age if names are equal
let result = by_name.then(by_age);     // Less — by_name decided it

PartialOrd — Partial Orderings

PartialOrd is the trait the compiler calls when it sees a < b. Its definition in the standard library looks like this (simplified):

pub trait PartialOrd<Rhs = Self>: PartialEq<Rhs>
where
    Rhs: ?Sized,
{
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
    fn lt(&self, other: &Rhs) -> bool { ... }
    fn le(&self, other: &Rhs) -> bool { ... }
    fn gt(&self, other: &Rhs) -> bool { ... }
    fn ge(&self, other: &Rhs) -> bool { ... }
}

The only required method is partial_cmp. The four comparison operators are provided methods built on top of it: lt calls partial_cmp and checks if the result is Some(Less), and so on. Because partial_cmp returns an Option, the operator a < b will evaluate to false when partial_cmp returns None. That is why NaN < 5.0 is false in Rust — the comparison is not meaningful, so it returns None, and the operator treats None as “not less.”

NaN and Partial Order:

Floating-point types implement PartialOrd but not Ord precisely because NaN is not comparable to anything, including itself. f32::partial_cmp returns None whenever either operand is NaN. This is the canonical example of a partial order in the standard library.

When you write a PartialOrd implementation, you need to decide which pairs of values are incomparable. For many types, all pairs are comparable, and partial_cmp never returns None. If that is the case, you should also implement Ord.

Ord — Total Orderings

Ord is for types where every two values can be placed in a definite order, and that order is consistent across the entire type. Its definition:

pub trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;
    fn max(self, other: Self) -> Self { ... }
    fn min(self, other: Self) -> Self { ... }
    fn clamp(self, min: Self, max: Self) -> Self { ... }
}

The single required method is cmp. It must return an Ordering — there is no room for None. The rest of the comparison operators are inherited from PartialOrd, but when a type implements Ord, the standard library’s generic code (sorting, BTreeMap, BinaryHeap) will typically call cmp rather than partial_cmp. The Ord documentation states that cmp and partial_cmp must agree: whenever partial_cmp returns Some(ordering), cmp must return the same ordering.

Inconsistent implementations cause broken sorting:

If you implement PartialOrd and Ord with different semantics, standard library sorting will produce garbage. Sorting algorithms use Ord::cmp, but the comparison operators (<, >) use PartialOrd::partial_cmp. A type that sorts one way with sort() and compares differently with < violates user expectations and can cause data structures like BTreeMap to behave unpredictably. The Rust standard library explicitly requires consistency, as discussed in issue #41270.

Implementing Ordered Comparisons

You have two main paths: let the compiler generate the implementations with derive, or write them by hand when the derived behavior does not match your domain semantics.

Deriving PartialOrd and Ord

For structs with fields that all implement PartialOrd and Ord, you can add #[derive(PartialOrd, Ord)] and get lexicographic field-by-field comparison in declaration order. The same is true for enums: variants are ordered by declaration, from smallest to largest.

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Score {
    points: u32,
    level: u8,
}
let s1 = Score { points: 100, level: 3 };
let s2 = Score { points: 90,  level: 5 };
assert!(s1 > s2); // points compared first: 100 > 90, level ignored

The derive macro works mechanically. It is perfect when the natural field order matches the ordering your type should have. But if you need a different comparison logic — say, comparing by a computed value, or ordering by one field descending and another ascending — you must implement the traits manually.

Manual Implementation of Ord

Ord is the trait you reach for when you want your type to be usable in BTreeMap, BinaryHeap, or any code that requires a total order. The manual implementation is almost always paired with a derived PartialEq and Eq, and either a derived or hand-written PartialOrd.

Here is a realistic example: a Player struct that needs to be sorted by score descending, with name as a tiebreaker ascending.

1

Step 1: Derive PartialEq and Eq

These are the equality traits that Ord requires. The derive macro handles them correctly for almost all structs.

#[derive(Debug, PartialEq, Eq)]
struct Player {
    name: String,
    score: u32,
}
2

Step 2: Derive or implement PartialOrd

You can derive PartialOrd if the derived order will match your Ord implementation. In our case the derived order would compare name first, then score ascending — not what we want. So we implement PartialOrd manually to delegate to Ord::cmp, guaranteeing consistency.

use std::cmp::Ordering;
impl PartialOrd for Player {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

Delegating partial_cmp to cmp:

This is the simplest way to ensure PartialOrd and Ord never diverge. Your partial_cmp always returns Some, wrapping the total order from cmp.

3

Step 3: Implement Ord with cmp

Now write the actual ordering logic. We compare scores in reverse order (higher first), then names in natural order.

impl Ord for Player {
    fn cmp(&self, other: &Self) -> Ordering {
        // Reverse score comparison: bigger score = Less (comes first)
        other.score.cmp(&self.score)
            .then_with(|| self.name.cmp(&other.name))
    }
}

The then_with closure is only called if the score comparison returned Equal, avoiding unnecessary work.

Let’s verify:

let mut players = vec![
    Player { name: "Alice".into(), score: 100 },
    Player { name: "Bob".into(),   score: 200 },
    Player { name: "Charlie".into(), score: 100 },
];
players.sort();
// Bob first (200), then Alice and Charlie (both 100, alphabetically)
assert_eq!(players[0].name, "Bob");
assert_eq!(players[1].name, "Alice");
assert_eq!(players[2].name, "Charlie");

Manual Implementation of PartialOrd for Non-Total Orders

Sometimes your type has genuinely incomparable values. The standard example beyond floats is a two-dimensional point with a product order: (x1, y1) is less than (x2, y2) only if both x1 <= x2 and y1 <= y2. Points like (1, 5) and (2, 3) are incomparable — neither is less than the other, and they are not equal.

use std::cmp::Ordering;
#[derive(Debug, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,
}
impl PartialOrd for Point {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self == other {
            Some(Ordering::Equal)
        } else if self.x <= other.x && self.y <= other.y {
            Some(Ordering::Less)
        } else if self.x >= other.x && self.y >= other.y {
            Some(Ordering::Greater)
        } else {
            None
        }
    }
}

Here, Point does not implement Ord because no total order can be defined that is consistent with this partial order. You cannot use sort() on a Vec<Point> directly; you would need sort_by with a custom comparator that picks a total order (like lexicographic) or panics on incomparable pairs.

PartialOrd without Ord limits standard library usage:

Types that only implement PartialOrd cannot be used in BTreeMap keys, BinaryHeap, or the default slice::sort. You must provide a custom comparator with sort_by or sort_by_key whenever you need a total order.

When to Choose PartialOrd vs Ord

The decision tree is small but important:

  • If every possible pair of values has a clear Less, Equal, or Greater relationship, implement Ord. This is the majority case for business logic types: IDs, timestamps, version numbers, scores.
  • If some pairs are genuinely incomparable, implement only PartialOrd. This is rare outside mathematical abstractions, but it is the correct model for types like intervals that can overlap without containing one another, or sets where one is not a subset of the other.
  • If your type contains an f32 or f64 and you want to use standard sorting, do not implement Ord on it directly. Instead, use sort_by with a comparison that defines an explicit total order (like f32::total_cmp), or wrap the float in a newtype that implements Ord via the total-order comparison method.

f32 and f64 have total_cmp:

The methods f32::total_cmp and f64::total_cmp provide a total order that is consistent with the IEEE 754 totalOrder predicate. This orders every possible bit pattern, including NaN variants. If you need Ord for a struct containing a float, delegate to total_cmp.

Consistency Rules and Why They Matter

The Rust standard library relies on the following contract: if a type implements Ord, then partial_cmp must never return None, and whenever it returns Some(ord), cmp must return the same ord. Breaking this contract does not always cause an immediate panic, but it corrupts the internal invariants of BTreeMap, BinaryHeap, and the sort algorithms. A BTreeMap that uses Ord::cmp for insertion but PartialOrd::partial_cmp for range lookups will miss keys or loop infinitely if the two disagree.

From a design perspective, the split between PartialOrd and Ord is a concession to floating-point numbers. The trait system forces you to mark types that carry only a partial order, so generic code can decide whether to accept them. If you are writing a library that takes T: Ord, you are declaring “I need a total order,” and users with partial-order types will see a compiler error — exactly what should happen.

Using Ordered Comparisons in Practice

Once your type implements PartialOrd (and possibly Ord), the operators <, <=, >, >= work automatically. But the real power shows up when the standard library’s generic algorithms accept your type.

Sorting and Slices

A slice [T] has a sort method that requires T: Ord. If you only have PartialOrd, use sort_by with a closure, or sort_by_key with a projection that returns an Ord value.

let mut points = vec![Point { x: 1, y: 5 }, Point { x: 2, y: 3 }];
// points.sort(); // error: Point doesn't implement Ord
points.sort_by(|a, b| {
    // fallback total order: compare x first, then y
    a.x.cmp(&b.x).then(a.y.cmp(&b.y))
});

BTreeMap and BinaryHeap

Both BTreeMap and BinaryHeap require Ord on the key/element type. If you only have PartialOrd, you cannot use them directly. Wrapping the type in a newtype that implements Ord via a custom cmp is the standard approach.

Min, Max, and Clamp

The free functions std::cmp::min and std::cmp::max work on any T: Ord and return the smaller/larger of the two values. clamp restricts a value to an inclusive range.

let a = 10;
let b = 20;
assert_eq!(std::cmp::min(a, b), 10);
assert_eq!(std::cmp::max(a, b), 20);
assert_eq!(15.clamp(0, 10), 10);

There are also min_by and max_by that accept a comparator function, useful when you have a custom ordering but not Ord.

Reverse Ordering

The std::cmp::Reverse wrapper flips the direction of any Ord implementation. Wrapping a value in Reverse(value) makes max return the original minimum, and sort arrange elements in descending order.

use std::cmp::Reverse;
let mut numbers = vec![3, 1, 4, 1, 5];
numbers.sort_by_key(|&n| Reverse(n));
assert_eq!(numbers, vec![5, 4, 3, 1, 1]);

Reverse is a zero-cost wrapper; it only changes how the comparisons are interpreted, no extra allocation.

Common Pitfalls

Deriving Ord on structs with floats:

If your struct contains an f32 or f64 field and you derive Ord, the compiler will reject it because f32 does not implement Ord. You must either implement Ord manually using total_cmp or decide that the struct should only implement PartialOrd.

Forgetting to implement Eq when implementing Ord:

Ord requires Eq. If you derive Ord, you must also derive Eq. Leaving it out will produce a compiler error that points back to the missing trait bound.

Implementing PartialOrd and Ord differently:

This is the most subtle and dangerous mistake. Even if you never call < on your type and only ever use sort(), library code elsewhere may use partial_cmp. If partial_cmp returns None for some pairs but cmp returns a definite ordering, a BTreeMap may silently break. Always implement PartialOrd by delegating to cmp when you have a total order.

When you write a manual PartialOrd for a type that also has Ord, the safest pattern is exactly the one shown in the Player example: partial_cmp calls Some(self.cmp(other)). That guarantees consistency by construction.

Summary

PartialOrd and Ord are the traits that turn <, >, and friends from syntax into behavior. The split between them is not accidental — it reflects a real distinction between types where all comparisons make sense and types where some comparisons are meaningless. Most Rust types you write will be totally ordered, and you will derive Ord or implement it by chaining field comparisons with .then(). When you do encounter a genuine partial order, Rust gives you the tools to model it accurately, and the trait system prevents you from accidentally feeding it to algorithms that demand a total order.