Skip to content

← Generics and Traits step 3 of 24

Easy Primitives

Trait bounds and why f64 is not Ord

A bare type parameter T can do almost nothing. You cannot add it, print it, or compare it, because T might be any type and most types cannot do those things. A trait bound is how you buy back exactly the capabilities you need:

pub fn max_of<T: PartialOrd + Copy>(items: &[T]) -> Option<T>

Read T: PartialOrd + Copy as “whatever T turns out to be, it must implement both PartialOrd and Copy“. Inside the body you may then use > (from PartialOrd) and copy values around freely (from Copy) — and nothing else.

This is where Rust differs sharply from C++ templates. A C++ template is type-checked once per instantiation, so a mistake surfaces as a wall of errors pointing deep inside a library. Rust type-checks the generic body once, against the bounds, before it has ever seen a caller. If the body compiles, every legal caller works. That guarantee is the reason Rust’s generic errors are readable at all.

Your task

pub fn max_of<T: PartialOrd + Copy>(items: &[T]) -> Option<T>

pub fn extremes(ints: Vec<i64>, floats: Vec<f64>) -> (Option<i64>, Option<f64>)

max_of returns the largest element, or None for an empty slice. extremes calls it twice — once at T = i64, once at T = f64 — and returns both answers. One generic function, two instantiations, no duplicated logic.

The lesson: PartialOrd vs Ord

The starter writes the bound as T: Ord + Copy, which is what most people reach for. Compile it:

error[E0277]: the trait bound `f64: Ord` is not satisfied
   |
11 |     (max_of(&ints), max_of(&floats))
   |                     ------ ^^^^^^^ the trait `Ord` is not implemented for `f64`
   |
note: required by a bound in `max_of`
   |
 3 | pub fn max_of<T: Ord + Copy>(items: &[T]) -> Option<T> {
   |                  ^^^ required by this bound in `max_of`

E0277 is the error you will meet most often in generic Rust, and it is best read bottom-up. The first line states the failure; the note at the end tells you which bound in which signature asked for it. That final note is the actionable part. Train yourself to jump there first — this is a real skill, not a footnote.

And the fact itself is worth internalising. f64 implements PartialOrd but not Ord, because Ord promises a total order and floats have NaN: NaN < 1.0, NaN > 1.0 and NaN == 1.0 are all false. There is no consistent place to put it. PartialOrd promises only that a comparison may return “these are not ordered”. Widening the bound from Ord to PartialOrd is not a hack — it is an honest statement of what the algorithm needs.

(This problem’s inputs never contain NaN, so > behaves normally. When you do need a total order on floats, f64::total_cmp is the standard answer.)

Two neighbouring errors

  • Drop Copy from the bound and you get E0507, “cannot move out of … which is behind a shared reference”. That reads like a borrow error, but the cause is a missing bound: without Copy (or Clone), taking a value out of the borrowed slice is a move you are not allowed to make.
  • Use > on a T with no ordering bound at all and you get E0369, “binary operation > cannot be applied to type T“. Operators are trait methods; no trait, no operator.

Remember the grade is compile + tests + clippy -D warnings.