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
dyn Shapeis the type "some type implementingShape". Its size is unknown at compile time (squares and circles differ), so it is always used behind a pointer:&dyn Shape,Box<dyn Shape>.Box::new(x)puts a value on the heap and gives an owning pointer to it (module 13). ABox<Square>converts automatically toBox<dyn Shape>.- Each call
s.area()runs the right implementation for whatever the element really is.
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 Trait | dyn Trait | |
|---|---|---|
| decided | at compile time | at run time |
| code generated | one copy per concrete type | one copy |
| call cost | direct call, can be inlined | indirect call via vtable |
| mixed types in one collection | no | yes |
| binary size | larger | smaller |
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
Vec<impl Trait>for mixed types: impossible; useVec<Box<dyn Trait>>.dyn Traitwithout a pointer:fn f(s: dyn Shape)is E0277 "the size for values of typedyn Shapecannot be known at compilation time". Use&dyn ShapeorBox<dyn Shape>.- Returning different types from
-> impl Trait: E0308 "ifandelsehave incompatible types", with a note suggestingBox<dyn Trait>. - Missing bound: calling
x.area()on aTwithoutT: Shapeis E0599 "no method namedareafound for type parameterT", with help suggesting the bound.
Recap
fn f<T: Trait>(x: T)andfn f(x: impl Trait)accept any implementing type; multiple bounds with+, long ones inwhere.-> impl Traitreturns one unnamed concrete type (iterators, closures).dyn Traitis a type-erased trait object, used behind&orBox; it allows mixed-type collections.- Trait objects are fat pointers (data + vtable); calls are indirect.
- Prefer generics; reach for
dynfor heterogeneity and run-time choice.
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>>.