RustArena

Trait objects vs generics in Rust: dyn Trait vs impl Trait

When to use generics (impl Trait, static dispatch) and when to use trait objects (dyn Trait, Box<dyn Trait>, dynamic dispatch) in Rust.

Rust gives you two ways to write code that works with "any type implementing Shape":

fn print_area(s: &impl Shape)       // generics: static dispatch
fn print_area(s: &dyn Shape)        // trait object: dynamic dispatch

Both compile and print the same thing. The difference is when the compiler decides which area method to call.

Generics: static dispatch

With impl Shape or <T: Shape>, the compiler generates a separate copy of the function for each concrete type (monomorphization). Calls are direct and can be inlined, so it's as fast as hand-written code. But each use has one concrete type: a Vec<T> holds only circles, or only squares.

Trait objects: dynamic dispatch

dyn Shape means "some type implementing Shape, decided at run time". A trait object is always behind a pointer (&dyn Shape, Box<dyn Shape>, Arc<dyn Shape>), stored as two pointers: one to the data, one to a vtable, a table of that type's method addresses. Each call looks up the method in the table, a small cost that prevents inlining.

What you get in return: different types in one collection.

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

fn main() {
    let shapes = vec![Circle(1.0), Square(2.0)];
    println!("{}", shapes.len());
}
error[E0308]: mismatched types

A Vec holds one type, so Circle and Square can't share it. Vec<Box<dyn Shape>> can: vec![Box::new(Circle(1.0)), Box::new(Square(2.0))].

The same applies to return types: fn make(round: bool) -> impl Shape must return one concrete type on every path (otherwise: "if and else have incompatible types"). Return Box<dyn Shape> to choose at run time.

Choosing

Generics / impl Trait dyn Trait
Dispatch compile time, inlinable run time, via vtable
Mixed types in one collection no yes
Binary size grows per type used one copy
Trait requirements any trait must be dyn compatible
Typical use utility functions, iterators, closures, hot loops plugin lists, UI widgets, handlers, Box<dyn Error>

Default to generics. Reach for dyn when you need a heterogeneous collection, want to choose an implementation at run time (from config), or want to keep compile times and binary size down for rarely called code.

The third option: an enum

If you know every type up front (Circle, Square, Triangle), an enum Shape { Circle(f64), Square(f64) } with match is often the simplest and fastest choice: no boxing, no vtable, and the compiler checks every case. Trait objects win when other code (or other crates) must be able to add new types.

dyn compatibility

Not every trait can be a trait object. A trait whose methods are generic, return Self, or that requires Sized can't work through a vtable:

fn main() {
    let items: Vec<Box<dyn Clone>> = Vec::new();
    println!("{}", items.len());
}
error[E0038]: the trait `Clone` is not dyn compatible

(Older compilers call this "object safety".) Add where Self: Sized to a problematic method to exclude it from the vtable, or split the trait.

Example

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

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

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

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

// Static dispatch: one copy per concrete type.
fn describe(s: &impl Shape) -> String {
    format!("{} has area {:.2}", s.name(), s.area())
}

// Dynamic dispatch: one copy, decides at run time.
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn make(round: bool, size: f64) -> Box<dyn Shape> {
    if round { Box::new(Circle(size)) } else { Box::new(Square(size)) }
}

fn main() {
    println!("{}", describe(&Circle(1.0)));
    println!("{}", describe(&Square(2.0)));

    let shapes: Vec<Box<dyn Shape>> = vec![make(true, 1.0), make(false, 2.0), make(false, 0.5)];
    for s in &shapes {
        println!("- {}", s.name());
    }
    println!("total area {:.2}", total_area(&shapes));
}

Output:

circle r=1 has area 3.14
square side=2 has area 4.00
- circle r=1
- square side=2
- square side=0.5
total area 7.39

Practice it