Generics, Traits and Lifetimes · about 45 min

Trait Bounds, impl Trait and dyn Trait

Why this matters

Traits become useful when code can accept any type that implements one: a function that prints anything displayable, a list of shapes of different kinds, a sort that works on anything comparable. Rust has two ways to do this. Generics with trait bounds (static dispatch) generate specialized code for each type, with no run-time cost. Trait objects (dyn Trait, dynamic dispatch) use one piece of code that looks up methods at run time, which lets different types share a collection. Knowing which to use is a core Rust design skill.

Trait bounds

A bound says "T can be any type that implements this trait":

use std::fmt::Display;

trait Shape {
    fn area(&self) -> f64;
}

struct Square(f64);
struct Circle(f64);

impl Shape for Square {
    fn area(&self) -> f64 { self.0 * self.0 }
}
impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.0 * self.0 }
}

fn report<T: Shape>(label: &str, s: &T) {
    println!("{label}: {:.2}", s.area());
}

fn show_twice<T: Display + Clone>(x: T) {
    let y = x.clone();
    println!("{x} {y}");
}

fn main() {
    report("square", &Square(2.0));
    report("circle", &Circle(1.0));
    show_twice("hi");
    show_twice(7);
}
square: 4.00
circle: 3.14
hi hi
7 7

T: Display + Clone requires both traits. When bounds get long, move them to a where clause, which reads better:

fn summarize<K, V>(map: &BTreeMap<K, V>) -> String
where
    K: Display + Ord,
    V: Display,
{ ... }

impl Trait: shorthand for arguments and returns

In argument position, impl Trait is sugar for a generic parameter:

trait Shape {
    fn area(&self) -> f64;
}

struct Square(f64);
impl Shape for Square {
    fn area(&self) -> f64 { self.0 * self.0 }
}

fn report(s: &impl Shape) {
    println!("{:.2}", s.area());
}

fn make_square(side: f64) -> impl Shape {
    Square(side)
}

fn evens(limit: u32) -> impl Iterator<Item = u32> {
    (0..limit).filter(|n| n % 2 == 0)
}

fn main() {
    report(&Square(3.0));
    report(&make_square(1.5));
    let v: Vec<u32> = evens(10).collect();
    println!("{:?}", v);
}
9.00
2.25
[0, 2, 4, 6, 8]

In return position, -> impl Trait means "this returns some specific type that implements the trait, but I am not telling you which". It is how functions return iterators and closures, whose real types are long or unnameable (module 11). The function must still return one concrete type: returning a Square on one path and a Circle on another is an error. For that, you need dyn.

dyn Trait: trait objects

Suppose you want a list holding circles and squares. A Vec<T> holds one type T. The solution is a trait object: a pointer to "some value implementing Shape", with the concrete type forgotten:

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
}

struct Square(f64);
struct Circle(f64);

impl Shape for Square {
    fn area(&self) -> f64 { self.0 * self.0 }
    fn name(&self) -> String { format!("square {}", self.0) }
}
impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.0 * self.0 }
    fn name(&self) -> String { format!("circle {}", self.0) }
}

fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    let mut sum = 0.0;
    for s in shapes {
        sum += s.area();
    }
    sum
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Square(2.0)),
        Box::new(Circle(1.0)),
        Box::new(Square(0.5)),
    ];
    for s in &shapes {
        println!("{:<12} {:.2}", s.name(), s.area());
    }
    println!("total {:.2}", total_area(&shapes));
}
square 2     4.00
circle 1     3.14
square 0.5   0.25
total 7.39

How dynamic dispatch works

A &dyn Shape or Box<dyn Shape> is a fat pointer: a pointer to the data and a pointer to a vtable, a table of function addresses generated by the compiler, one per (type, trait) pair:

 Box<dyn Shape>                       vtable for (Circle, Shape)
 +-----------+                        +---------------------------+
 | data  --------> Circle(1.0)        | drop:  <Circle drop code> |
 | vtable -------------------------> | size, align               |
 +-----------+                        | area:  Circle::area       |
                                      | name:  Circle::name       |
                                      +---------------------------+
use std::mem::size_of;

trait Shape {
    fn area(&self) -> f64;
}

fn main() {
    println!("&u8:        {}", size_of::<&u8>());
    println!("&dyn Shape: {}", size_of::<&dyn Shape>());
    println!("Box<dyn Shape>: {}", size_of::<Box<dyn Shape>>());
}
&u8:        8
&dyn Shape: 16
Box<dyn Shape>: 16

s.area() loads the function address from the vtable and calls it: one extra memory load and an indirect call, and the compiler cannot inline across it. Usually negligible, but it is a real cost compared with static dispatch.

Static versus dynamic dispatch

generics / impl Traitdyn Trait
decidedat compile timeat run time
code generatedone copy per concrete typeone copy
call costdirect call, can be inlinedindirect call via vtable
mixed types in one collectionnoyes
binary sizelargersmaller

Rule of thumb: use generics by default; use dyn when you need a collection of different types, want to choose an implementation at run time (plugins, configuration), or want to keep compile times and binary size down.

Not every trait can be made into an object: methods that return Self or have generic type parameters make a trait not "dyn compatible", because the vtable could not contain them. The compiler explains it (E0038) when it happens.

Common mistakes

Recap

Going further: the Rust Book chapter 18.2, "Using Trait Objects That Allow for Values of Different Types", builds a GUI-style example with Vec<Box<dyn Draw>>.

Exercises

Exercise 1 · write code

A generic function with bounds

Write fn print_sorted<T: Ord + std::fmt::Display>(items: &mut Vec<T>) that sorts the vector and prints the items on one line separated by ,. The given main calls it with integers and with Strings:

1, 2, 3, 10
apple, banana, cherry

File: main.rs

Starting point

fn main() {
    let mut nums = vec![10, 2, 3, 1];
    print_sorted(&mut nums);
    let mut words = vec![String::from("cherry"), String::from("apple"), String::from("banana")];
    print_sorted(&mut words);
}

Example

expected output

1, 2, 3, 10
apple, banana, cherry
Hint 1

sort needs T: Ord; to_string() or {} needs T: Display.

Hint 2

Build the line by converting each item with to_string(), collecting into a Vec<String>, and joining.

Reference solution
use std::fmt::Display;

fn print_sorted<T: Ord + Display>(items: &mut Vec<T>) {
    items.sort();
    let parts: Vec<String> = items.iter().map(|x| x.to_string()).collect();
    println!("{}", parts.join(", "));
}

fn main() {
    let mut nums = vec![10, 2, 3, 1];
    print_sorted(&mut nums);
    let mut words = vec![String::from("cherry"), String::from("apple"), String::from("banana")];
    print_sorted(&mut words);
}

Exercise 2 · write code

A heterogeneous list with dyn

The trait Animal has fn name(&self) -> String and fn sound(&self) -> String. Implement it for Dog ("Rex", "woof"), Cat ("Tom", "meow") and Cow ("Bess", "moo"). Then complete make to return a Box<dyn Animal> for the words dog, cat, cow (and None for anything else). The given main reads words from stdin and prints:

Rex says woof
Tom says meow
unknown animal: fox
Bess says moo

for the input dog cat fox cow (one per line).

File: main.rs

Starting point

use std::io;

trait Animal {
    fn name(&self) -> String;
    fn sound(&self) -> String;
}

struct Dog;
struct Cat;
struct Cow;

fn make(kind: &str) -> Option<Box<dyn Animal>> {
    None
}

fn main() {
    let mut zoo: Vec<Box<dyn Animal>> = Vec::new();
    for line in io::stdin().lines() {
        let line = line.unwrap();
        match make(line.trim()) {
            Some(a) => zoo.push(a),
            None => println!("unknown animal: {}", line.trim()),
        }
    }
}

Example

input

dog
cat
fox
cow

expected output

Rex says woof
Tom says meow
unknown animal: fox
Bess says moo
Hint 1

Three impl Animal for ... blocks.

Hint 2

make returns Some(Box::new(Dog)) and so on: a Box<Dog> converts to Box<dyn Animal> automatically.

Hint 3

The starter's main never prints the animals; print NAME says SOUND when one is made, before pushing it.

Reference solution
use std::io;

trait Animal {
    fn name(&self) -> String;
    fn sound(&self) -> String;
}

struct Dog;
struct Cat;
struct Cow;

impl Animal for Dog {
    fn name(&self) -> String { String::from("Rex") }
    fn sound(&self) -> String { String::from("woof") }
}

impl Animal for Cat {
    fn name(&self) -> String { String::from("Tom") }
    fn sound(&self) -> String { String::from("meow") }
}

impl Animal for Cow {
    fn name(&self) -> String { String::from("Bess") }
    fn sound(&self) -> String { String::from("moo") }
}

fn make(kind: &str) -> Option<Box<dyn Animal>> {
    match kind {
        "dog" => Some(Box::new(Dog)),
        "cat" => Some(Box::new(Cat)),
        "cow" => Some(Box::new(Cow)),
        _ => None,
    }
}

fn main() {
    let mut zoo: Vec<Box<dyn Animal>> = Vec::new();
    for line in io::stdin().lines() {
        let line = line.unwrap();
        match make(line.trim()) {
            Some(a) => {
                println!("{} says {}", a.name(), a.sound());
                zoo.push(a);
            }
            None => println!("unknown animal: {}", line.trim()),
        }
    }
}

Exercise 3 · quiz

Static or dynamic?

Which statements are true? Select all that apply.

Answer
  • fn f(x: &impl Shape) is compiled once per concrete type used, with direct calls (static dispatch)

  • fn g(x: &dyn Shape) is compiled once; method calls go through a table of function pointers (dynamic dispatch)

  • A &dyn Shape is a fat pointer: data pointer plus vtable pointer

impl Trait in argument position is a generic parameter: monomorphized, one type per call site. dyn Trait erases the concrete type: one compiled function, and each method call looks up the address in the vtable. Only dyn allows mixing types in one collection (Vec<Box<dyn Shape>>); a Vec<T> holds a single concrete T.