Rust Trait Objects
A trait object is one value seen only through a trait it implements. Its real type can differ from the next value's, so one Vec can hold mixed kinds.
One Vec, one kind only
A Vec holds one kind of value, so a Circle and a Square cannot share one:
let shapes = vec![Circle { radius: 1.0 }, Square { side: 2.0 }];
// error[E0308]: mismatched types โ the Vec expects one kindOne list, mixed kinds
Circle and Square are different types, yet one Vec holds both. dyn Shape reads as: some value that implements Shape โ the real type is known only while running.
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes {
println!("area = {:.2}", s.area());
}The pointer is fat: two addresses
A trait object pointer stores two addresses. One points to the value. One points to a vtable. The vtable is the table of that type's method code. Calling a method through the vtable is dynamic dispatch. The generics way is static dispatch. Generics compile one copy per type. A plain Box<T> is 8 bytes โ one pointer.
println!("pointer size = {}", std::mem::size_of::<Box<dyn Shape>>());Borrow one instead of owning
A trait object can only live behind a pointer. &dyn Shape borrows a value that stays exactly where it was โ nothing is allocated.
fn print_area(shape: &dyn Shape) {
println!("area = {:.2}", shape.area());
}Example
trait Shape { fn area(&self) -> f64; }
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.radius * self.radius } }
impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } }
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes {
println!("area = {:.2}", s.area());
}
println!("pointer size = {}", std::mem::size_of::<Box<dyn Shape>>());
}Key points
- Object safety sets the rules.
- Only some traits can become objects.
- Their methods must run through a pointer.
- No method may return Self by value.
- No method may be generic.
- The compiler calls these rules dyn compatibility.
- Error E0038 says a trait โis not dyn compatibleโ.
- A call through a vtable can never be inlined.
- The compiler does not know which concrete code will run.
- So it cannot fold the call into its caller.
- That is one reason trait objects lose to generics.
- They lose in tight loops.
- A trait object forgets the concrete type on purpose.
- There is no cast back.
- Recovering it needs the std::any::Any trait.
- It also needs a downcast.
- That only works when the trait was designed for it.
- Choose trait objects when types must mix in one place, or when the choice is made while the program runs.
In this example, Circle and Square are different structs, but both implement Shape. The Vec holds Box<dyn Shape> values. So the two different types can sit in one list. Each Box owns its shape. The shape sits on the heap. When the loop calls s.area(), Rust looks the method up. It looks in that value's vtable. Then it runs the right code. The circle gives 3.14. The square gives 4.00. The last line measures the lesson. A Box<dyn Shape> is 16 bytes. It carries the vtable address. That address sits beside the data address. Rust Object Safety Rules lists every breaker and gives its fix.
Note
Choose generics when one type at a time is enough (faster, checked per type).
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Rust Playground
Exercise
Why must dyn Shape values live behind a pointer like Box?