Defining Trait Objects
Learn how to define and create trait objects in Rust using dyn Trait for runtime polymorphism and heterogeneous collections
A trait object is a dynamically‑dispatched value that points to both an instance of a type implementing a trait and to a table of the trait’s methods for that type. You write them as dyn Trait behind a pointer, like &dyn Draw, Box<dyn Draw>, or Arc<dyn Draw>. They let you treat different concrete types uniformly through a shared trait interface, even when the compiler cannot know every possible type ahead of time.
The mechanism answers a specific need: storing different types together in the same collection, or passing an object to a function whose concrete type is determined only at runtime. In Rust, the size of every type must be known at compile time unless it lives behind a pointer. Trait objects solve this by wrapping the actual data behind a pointer and attaching a virtual method table (vtable) to the pointer, so the compiler can work with a fixed‑size “fat pointer” and still invoke the right method.
Creating a Trait Object Explicitly
You can turn a concrete type into a trait object with an as cast. Suppose you have a trait Greet and two structs that implement it:
trait Greet {
fn say_hello(&self) -> String;
}
struct Person { name: String }
struct Robot { id: u32 }
impl Greet for Person {
fn say_hello(&self) -> String {
format!("Hi, I'm {}.", self.name)
}
}
impl Greet for Robot {
fn say_hello(&self) -> String {
format!("Beep boop, unit {}.", self.id)
}
}
Now &Person can become &dyn Greet with an explicit cast:
let alice = Person { name: "Alice".into() };
let greeter: &dyn Greet = &alice as &dyn Greet;
println!("{}", greeter.say_hello());
The as &dyn Greet cast tells the compiler to erase the concrete type Person and treat the reference as a trait object. The same works for Box:
let bot = Box::new(Robot { id: 42 });
let greeter: Box<dyn Greet> = bot as Box<dyn Greet>;
Behind the scenes the cast builds the fat pointer: a pointer to the Person (or Robot) data, plus a pointer to the Greet vtable for that type. The vtable contains a function pointer for say_hello and a destructor, among other things.
Casting an unsized value directly:
You cannot write let obj: dyn Greet = alice; — dyn Greet is unsized. It must always live behind a pointer such as &, Box, Rc, or Arc.
Implicit Coercions
Rust often coerces a concrete reference to a trait object automatically. The same &alice above works without an explicit cast when the context expects a &dyn Greet:
fn announce(who: &dyn Greet) {
println!("{}", who.say_hello());
}
let alice = Person { name: "Alice".into() };
announce(&alice); // &Person coerces to &dyn Greet
The coercion happens at function call sites, let bindings with explicit type annotations, and when assigning to a field of type Box<dyn Trait>. This “type erasure” is key to Rust’s dynamic polymorphism — it lets you forget the concrete type while preserving the ability to call trait methods.
The dyn keyword is required in modern Rust:
In Rust 2015 you could write &Greet without the dyn keyword. Since Rust 2018, the bare‑trait‑object syntax is deprecated. Always use dyn Trait for trait objects to make the dynamic dispatch explicit.
Building Heterogeneous Collections
A typical reason to reach for trait objects is when you need a single collection that contains values of different concrete types, all sharing a trait. With generics, a Vec<T> forces every element to be the exact same T. Trait objects circumvent that restriction because Box<dyn Greet> is a single, fixed‑size type (two usizes) regardless of what it points to.
let speakers: Vec<Box<dyn Greet>> = vec![
Box::new(Person { name: "Alice".into() }),
Box::new(Robot { id: 42 }),
Box::new(Person { name: "Bob".into() }),
];
for speaker in &speakers {
println!("{}", speaker.say_hello());
}
Each Box::new(...) allocates a concrete value on the heap and then Box<T> is coerced to Box<dyn Greet> when inserted into the vector. Iteration calls say_hello through the vtable without caring whether the current element is a Person or a Robot.
It compiles — dynamic dispatch in action:
If this code builds without errors, you have successfully created your first heterogeneous collection of trait objects. The compiler ensures that every element implements Greet, and the vtable lookup at runtime always calls the correct method.
What a Trait Object Looks Like in Memory
A trait object, such as &dyn Greet, is a fat pointer: two machine words wide.
- data pointer – addresses the concrete value (the
PersonorRobotinstance). - vtable pointer – addresses a static vtable generated by the compiler for the combination
impl Greet for Person.
The vtable holds:
- a pointer to each method of the trait (e.g.,
say_hello), - a destructor (drop glue) for the concrete type,
- the size and alignment of the concrete type (used for deallocation and future layout optimizations).
A simplified representation for Person could be visualised like this (pseudo‑layout, plain‑text diagram):
+-------------------+ +--------------------+
| &dyn Greet | | Person { name } |
+-------------------+ +--------------------+
| data ptr ---------|------>| "Alice" |
| vtable ptr ------|--+ +--------------------+
+-------------------+ |
| +-------------------------------------+
+--->| vtable for impl Greet for Person |
+-------------------------------------+
| destructor: free Person memory |
| size: 24 (on 64-bit) |
| align: 8 |
| say_hello: &<Person as Greet>::say_hello |
+-------------------------------------+
When you call greeter.say_hello(), the compiler emits code that follows the vtable pointer, indexes into the function‑pointer slot for say_hello, and calls it with the data pointer as the self argument. This indirection is what people mean by “dynamic dispatch”.
Returning Trait Objects from Functions
Sometimes the concrete type to return depends on a runtime decision. A function can return a Box<dyn Trait> to hide the implementation detail:
fn make_greeter(kind: &str, name: &str) -> Box<dyn Greet> {
match kind {
"person" => Box::new(Person { name: name.to_string() }),
"robot" => Box::new(Robot { id: name.len() as u32 }),
_ => panic!("unknown kind"),
}
}
let g = make_greeter("robot", "R2D2");
println!("{}", g.say_hello());
The caller receives a trait object and only knows it implements Greet. The concrete allocation and type remain hidden inside the function. This pattern is common in plugin systems or factory methods where the caller doesn’t need to know (or can’t know at compile time) the exact type.
Trait objects require object‑safe traits:
Not every trait can be made into a trait object. A trait must be object‑safe. If you return a Box<dyn Trait> and the compiler complains about Self in a method or about generic parameters, your trait likely fails the object‑safety rules.
Storing Trait Objects in Structs
Structs can hold trait objects to delay the choice of concrete type until runtime, or to own a value whose type is erased.
struct App {
greeter: Box<dyn Greet>,
}
impl App {
fn new(greeter: Box<dyn Greet>) -> Self {
App { greeter }
}
fn run(&self) {
println!("{}", self.greeter.say_hello());
}
}
let app = App::new(Box::new(Person { name: "Alice".into() }));
app.run();
You can also store a reference with a lifetime, like &'a dyn Greet, but then the struct itself must be tied to that lifetime. Box<dyn Greet> is more common because it owns the trait object and doesn’t impose a borrow.
Common Mistakes and Their Fixes
- Trying to use
dyn Traiton the stack —let x: dyn Greet = ...is illegal because the compiler doesn’t know the size. Wrap it inBox,&, or another pointer. - Forgetting
'staticbounds on owned trait objects —Box<dyn Greet>implicitly requires'staticunless you explicitly writeBox<dyn Greet + 'a>. If the concrete type contains non‑'staticreferences, you’ll see a confusing lifetime error. Make sure the data lives long enough, or use+ 'ato relax the bound. - Cloning a trait object —
Box<dyn Greet>does not implementClonebecause cloning an arbitrary trait object isn’t guaranteed to be safe. If you need cloning, define a separate trait likeCloneBoxthat returnsBox<dyn CloneBox>and implement it for allT: Clone. - Mixing generics and trait objects without understanding the trade‑off — Generics (static dispatch) and trait objects (dynamic dispatch) are different tools. Using a trait object when all callers are known at compile time introduces a small runtime cost for indirection. Conversely, forcing everything into generics can lead to binary bloat and inflexible runtime behaviour.
Where Trait Objects Appear in Real Code
Trait objects show up naturally in designs that need to swap behaviour at runtime. Examples include:
- GUI frameworks that keep a list of drawable widgets:
Vec<Box<dyn Widget>>. - Logging and metrics libraries that accept a
Box<dyn Sink>to let users plug in different backends. - State machines where each state is a trait object implementing
State, and transitions return aBox<dyn State>. - Event‑driven systems that store callbacks as
Box<dyn FnMut(...)>(closure trait objects).
Every time you see Box<dyn SomeTrait> in a function signature or a struct field, you are looking at trait objects providing polymorphism without inheritance.
Summary
You define a trait object by writing dyn Trait behind a pointer, like &dyn Trait or Box<dyn Trait>. The concrete type is erased, leaving only the trait interface. Rust constructs a fat pointer that combines a data pointer and a vtable pointer; method calls go through the vtable for dynamic dispatch. Trait objects enable heterogeneous collections, runtime‑determined return types, and APIs that hide implementation details — all without inheritance or garbage collection.