15.2Generic functions

Last updated June 13, 2026

The last lesson introduced the type parameter <T> and ended on a function that doesn't compile. This lesson writes generic functions that do, sees exactly what kind of generic code the compiler accepts on its own, and then meets the error that largest produces, which sets up chapter 16.

The syntax

A generic function declares its type parameters in angle brackets after the name, then uses them in the parameter and return types:

fn first_of_pair<T>(pair: (T, T)) -> T {
    pair.0
}

fn main() {
    let a = first_of_pair((1, 2));
    let b = first_of_pair(('x', 'y'));
    println!("{a} {b}");
}
1 x

first_of_pair is generic over T. It takes a pair of Ts and returns the first one. The compiler figures out what T is at each call: i32 for (1, 2), char for ('x', 'y'). You wrote the logic once, and it serves both. You can have several type parameters, too: fn pair<T, U>(a: T, b: U) -> (T, U) is generic over two independent types.

This compiles with no fuss because of what the body does: it only moves a value around (pair.0 hands back one of the values). It never compares, prints, or computes with the T. That's the key to which generics need nothing extra.

What compiles without bounds

A generic function compiles on its own as long as its body only does things that work for every possible type: storing a value, moving it, returning it, putting it in a tuple, passing it to another function that's equally generic. These operations don't care what T is, so the compiler is satisfied that the function truly works for any T.

fn swap<T>(a: T, b: T) -> (T, T) {
    (b, a)
}

fn triple<T: Clone>(x: T) -> (T, T, T) {
    (x.clone(), x.clone(), x)
}

swap just reorders two values; it works for any T, no bound needed. triple is the first hint of what's coming: it calls x.clone(), and cloning isn't something every type can do (lesson 8.7), so it must say T: Clone, read "T is some type that can be cloned." That : Clone is a trait bound, a promise about T's capabilities, and the full story is chapter 16. We show it here only so you recognize that the moment a generic body does something to a T, a bound usually appears.

The largest error, at last

Now the function from lesson 15.1, and the exact error it produces:

fn largest<T>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:2:10
  |
2 |     if a > b { a } else { b }
  |        - ^ - T
  |        |
  |        T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
  |             ++++++++++++++++++++++

There it is, precisely the diagnosis from last lesson. The body uses > on two Ts, and the compiler objects: a bare T is any type, and not every type supports >. Read the help block as the compiler reading ahead in the syllabus (the same way it pointed at &mut in lesson 9.1): it suggests T: std::cmp::PartialOrd, a trait bound saying "restrict T to types that can be ordered." PartialOrd is the trait for the comparison operators, and adding that bound is exactly what chapter 16 does to make largest compile. The error message is, quite literally, the table of contents for the next chapter.

Key insight

A generic function compiles without bounds only while its body treats T as an opaque thing to move around. The instant the body uses a capability (>, ==, +, printing, cloning), the compiler demands a bound proving T has that capability. This is the line between this chapter and the next: generics give you the <T> placeholder; trait bounds (chapter 16) let the body actually do things with it. Until then, generic functions in this chapter only store and pass values.

Turbofish: naming the type explicitly

Usually the compiler infers T from the arguments. When it can't (because nothing in the call pins it down), you name it explicitly with the turbofish ::<T>, the syntax you first glimpsed with parse::<i32>() in lesson 5.6:

fn make_default<T: Default>() -> T {
    T::default()
}

fn main() {
    let x = make_default::<i32>();   // turbofish: T is i32
    let y: f64 = make_default();     // or let the binding's type decide
    println!("{x} {y}");
}
0 0

make_default returns a T but takes no T argument, so the compiler can't infer the type from a parameter; you supply it with make_default::<i32>() or by annotating the binding (let y: f64 = ...). The turbofish is the explicit answer to "which type?" when inference can't decide, and parse was a generic function all along, which is why it needed the turbofish or a type annotation back in chapter 5. (make_default uses a Default bound, chapter 16 again, because calling T::default() requires T to have a default.)

Quiz time

Question #1

Why does fn swap<T>(a: T, b: T) -> (T, T) { (b, a) } compile without any trait bound, while largest does not?

Show solution

swap only moves its values (it reorders them into a tuple), and moving works for every type, so no capability needs to be promised. largest uses a > b, which requires T to support comparison, and not every type does, so the compiler demands a bound (T: PartialOrd). The body's operations decide whether a bound is needed.

Question #2

What error does largest produce, and what does the compiler's help suggest?

Show solution

E0369: binary operation > cannot be applied to type T. The help suggests restricting the type parameter with a trait bound: fn largest<T: std::cmp::PartialOrd>(...). That bound (chapter 16) promises T supports ordering, which is what the > in the body needs.

Question #3

When do you need the turbofish (::<T>), and where have you used it before?

Show solution

When the compiler can't infer the type parameter from the call's arguments, typically because the type appears only in the return type (or nowhere in the parameters). You supply it explicitly with func::<Type>(), or by annotating the binding's type. You used it with parse::<i32>() in lesson 5.6; parse was a generic function all along.

Generic functions take a type parameter. So can structs and enums, and the next lesson reveals that two types you've used since chapter 11, Option<T> and Vec<T>, were generic structs and enums the whole time.