15.xChapter 15 summary and quiz
This chapter removed duplication across types, and set up traits. Review, then a quiz.
Quick review
Generics remove repetition across types, the way function parameters remove it across values (15.1). A type parameter <T> is a placeholder a caller fills in, named by convention with a single uppercase letter. A generic function declares its parameters in angle brackets after the name and uses them in its signature (15.2). A generic body compiles without bounds only while it treats T as opaque (store, move, return); the moment it uses a capability (>, ==, +, cloning, printing), the compiler demands a trait bound (chapter 16). The largest function is the standing example: it needs T: PartialOrd, and its E0369 error is literally chapter 16's table of contents. When inference can't pin down the type, the turbofish ::<T> names it explicitly (as parse::<i32>() always did).
Structs and enums take type parameters too (15.3), and the reveal is that Option<T>, Result<T, E>, and Vec<T> were generic all along, the <T> an ordinary type parameter. Type aliases (type) give a long generic type a short synonym (distinct from a newtype, which makes a new type), and const generics (<const N: usize>) range over compile-time values like array lengths. Methods on generic types use impl<T> Type<T> (15.4); writing a concrete impl Type<f64> instead gives a method to only that instantiation, the way sqrt-using methods exist only for f64 points.
Finally, monomorphization (15.5): the compiler turns one generic definition into a specialized copy per type used, at compile time, so generics are zero-cost at runtime, as fast as hand-written concrete code. The price is paid in binary size and compile time, not runtime. When you need one shared copy across many types at runtime, trait objects (lesson 16.9) are the alternative.
Quiz time
Question #1
What's the parallel between a type parameter and an ordinary function parameter?
Show solution
A function parameter is a placeholder for a value the caller supplies, letting one function body serve many values of a fixed type. A type parameter <T> is a placeholder for a type the caller supplies, letting one definition serve many types. Generics are to types what parameters are to values.
Question #2
Does each of these compile as written? If not, why?
a)
fn identity<T>(x: T) -> T { x }
b)
fn show<T>(x: T) { println!("{x}"); }
c)
struct Wrap<T> { value: T }Show solution
a) Compiles: it only returns the value, no capability needed. b) Does not compile: println!("{x}") needs T to be printable (the Display trait), so it requires a bound like T: std::fmt::Display (chapter 16). c) Compiles: a struct just stores a T; storing needs no bound.
Question #3
What does monomorphization do, and what does it cost?
Show solution
It generates a specialized copy of generic code for each concrete type used, at compile time, so the running program calls ordinary concrete functions with no runtime overhead (zero-cost). The cost is larger binary size (more copies) and longer compile time, both paid at build time, not at runtime.
Question #4
The exercise. Define a generic struct Stack<T> (a last-in-first-out stack) backed by a Vec<T>, with: new() returning an empty stack, push(&mut self, item: T), pop(&mut self) -> Option<T>, and len(&self) -> usize. Then use it with i32s in main: push 1, 2, 3, pop one, and print the length. (You don't need any trait bounds for this.)
Show solution
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Stack<T> {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn len(&self) -> usize {
self.items.len()
}
}
fn main() {
let mut stack = Stack::new();
stack.push(1);
stack.push(2);
stack.push(3);
stack.pop();
println!("length: {}", stack.len());
}length: 2
Design notes. Stack<T> is generic over the element type and stores a Vec<T> (itself generic, lesson 15.3). impl<T> Stack<T> declares the parameter so the methods are available for every element type. pop returns Option<T> because the stack might be empty (lesson 11.3), matching Vec::pop. No bounds are needed because the methods only store, move, and return T, never compare or print it. The compiler infers Stack<i32> from the push(1) calls; pushing a String instead would make a Stack<String> from the same definition. (Vec and its methods are chapter 18; here it's just the backing store.)
You can write code generic over types, but the body still can't do much with them. Chapter 16 lifts that ceiling: traits define shared behavior, trait bounds let a generic body compare, print, and compute with its type parameters, and largest finally compiles. It's the structural centerpiece of the language and the chapter where "Rust can be shorter than C++" is most dramatically true.