Intermediate · Modules, crates, traits and generics · topic 30 of 50 · 25 min
Trait objects and dynamic dispatch
Explanation
The problem generics cannot solve
A Vec<T> holds values of one type. What if a drawing app needs a list with circles, squares and text boxes together? With generics, T must be one type. A trait object solves this: it is a value whose exact type is hidden, and all we know is that it implements a trait. You write it as dyn Trait, always behind some kind of pointer, such as Box<dyn Trait> or &dyn Trait.
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String;
}
struct Circle { r: f64 }
struct Square { s: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { 3.14159 * self.r * self.r }
fn name(&self) -> String { "circle".into() }
}
impl Shape for Square {
fn area(&self) -> f64 { self.s * self.s }
fn name(&self) -> String { "square".into() }
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { r: 1.0 }),
Box::new(Square { s: 2.0 }),
];
for s in &shapes {
println!("{} {:.2}", s.name(), s.area());
}
}Why behind a pointer?
Circle and Square have different sizes. A Vec needs every item to be the same size. A Box (a pointer to heap memory, module 8) is always the same size, so a Vec of boxes works. dyn Shape on its own has no known size, which is why you never see a bare dyn Shape variable.
Dynamic dispatch and the vtable
Dispatch means choosing which function to call. With generics, the compiler knows the exact type and calls the right function directly: static dispatch. With trait objects, the type is only known at run time. Each Box<dyn Shape> is a "fat pointer" made of two pointers: one to the data, and one to a vtable, a small table of that type's method addresses. A call looks up the method in the table: dynamic dispatch.
Box<dyn Shape>
data ptr ---> Circle { r: 1.0 }
vtable ptr -> [ area: Circle::area, name: Circle::name, drop, size ]Generics or trait objects?
| Generics (impl Trait / T: Trait) | Trait objects (dyn Trait) | |
|---|---|---|
| Dispatch | Static, decided at compile time | Dynamic, through a vtable at run time |
| Speed | Fastest; can be inlined | Small cost per call; usually fine |
| Mixed types in one collection | No | Yes |
| Binary size | Grows with each type used | One copy of the code |
| Typical use | Algorithms, containers, hot loops | Plugins, UI widgets, handlers chosen at run time |
Which traits can be trait objects?
Not every trait can be used as dyn Trait. The rules were called "object safety" and are now called "dyn compatibility". In simple words: methods must take self by reference (&self or &mut self), must not return Self, and must not have their own generic type parameters. Clone, for example, is not dyn compatible because clone returns Self. The compiler gives a clear error if you break these rules.
Returning one of several types
fn make_shape(kind: &str) -> Box<dyn Shape> {
if kind == "circle" {
Box::new(Circle { r: 1.0 })
} else {
Box::new(Square { s: 1.0 })
}
}Common mistakes
- Writing Vec<dyn Shape>: "the size cannot be known at compile time". Use Vec<Box<dyn Shape>>.
- Forgetting the dyn keyword. In edition 2021 and later, a trait used as a type must be written dyn Trait.
- Reaching for dyn everywhere because it looks like Java. Prefer generics unless you need mixed types or run-time choice.
- Trying to make a trait object from a trait whose methods return Self or are generic.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Trait objects and dynamic dispatch · 1/6
A Vec of T holds one type. To mix circles and squares in one list, use trait objects.
Prefer a video by a teacher? Find free videos on “rust trait objects dyn box dynamic dispatch vs generics” on YouTube ↗
In real life: Payment providers chosen at run time
A checkout service supports several payment providers. Which one to use is read from configuration when the service starts, so the type is not known at compile time. The service stores a Box<dyn PaymentProvider>, and every request calls charge on it. Adding a new provider means a new struct with one impl block.
trait PaymentProvider {
fn charge(&self, paise: u64) -> Result<String, String>;
}
struct UpiGateway;
struct CardGateway;
impl PaymentProvider for UpiGateway {
fn charge(&self, paise: u64) -> Result<String, String> {
Ok(format!("UPI txn for {paise} paise"))
}
}
impl PaymentProvider for CardGateway {
fn charge(&self, paise: u64) -> Result<String, String> {
if paise > 20_000_000 { Err("card limit exceeded".into()) } else { Ok("card txn".into()) }
}
}
fn provider_from_config(name: &str) -> Box<dyn PaymentProvider> {
match name {
"upi" => Box::new(UpiGateway),
_ => Box::new(CardGateway),
}
}
fn main() {
let p = provider_from_config("upi");
println!("{:?}", p.charge(49_900));
}Practice (0/3 done)
Which type can hold a Circle and a Square in the same Vec?
A function must return a Circle or a Square depending on input. Which return type works?
Create a trait Discount with fn apply(&self, amount: u32) -> u32, and three structs: Flat(u32), Percent(u32) and NoDiscount. Store them in a Vec<Box<dyn Discount>>, apply each to Rs 1,000, and print the results. Then write the same with a generic function and explain in one line when you would choose each.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Packages, crates and modulesModules, crates, traits and generics · same module
- Box<T>: values on the heapClosures, iterators and smart pointers
- Interview questions: traits, errors, pointers and concurrencyRust careers and interviews
- Virtual functions and polymorphismC++ Programming (modern C++ and DSA) · Operator overloading, inheritance and polymorphism
- Generics: reusable, type-safe codeC# and .NET · Collections, generics and LINQ
- Generics: type parameters and constraintsGo Programming (backend and cloud) · Packages, testing and generics
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is dynamic dispatch?
Answer: Looking up the method through a vtable at run time
Trait objects use a vtable to find the method at run time.
Why must dyn Trait be behind a pointer like Box?
Answer: Its size is not known at compile time
Different implementing types have different sizes.
Which is usually faster in a tight loop?
Answer: Generics with a trait bound
Static dispatch allows inlining and avoids the vtable lookup.
Why can Clone not be used as dyn Clone?
Answer: clone returns Self, so it is not dyn compatible
Methods returning Self break dyn compatibility.
Which trait lets *b reach the value inside a Box?Related
What happens if you call a blocking function inside a tokio task?Related
Answer: It can stall other tasks on that worker thread
Blocking work belongs in spawn_blocking.
From Interview questions: traits, errors, pointers and concurrency
Are generics slower than hand-written code for each type?Related
Answer: No, monomorphization makes them as fast
Specialised copies are generated at compile time.
What is a trait?Related
Answer: A set of methods types can promise to implement
Traits describe shared behaviour.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Modules, crates, traits and generics”. The module exam comes next.
- Module 7 exam: Modules, crates, traits and generics · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Closures, iterators and smart pointers. 50 topics left in the course before the final exam.