Trait Objects and Dynamic Dispatch
Trait objects enable "dynamic dispatch", a form of polymorphism. They allow you to treat different concrete types that implement a specific trait as if they were the same at runtime. They are particularly useful when you need to create collections of different types that share common behavior, or when the exact type of an object isn't known until runtime.
Trait Objects Basics
Recall that a trait (also called contract or interface in other programming languages) represents a behavior that multiple types can implement. In the example below, the Draw
trait describes the ability to be drawn on a screen. A trait is at its core a set of function signatures (and associated types and constants).
A "trait object", denoted dyn SomeTrait
where dyn
is a keyword and SomeTrait
is a trait (or a set thereof, see below), is an object that implements that specific trait, but which underlying concrete type is not known at compile time. In the example below, dyn Draw
may be either a Button
or a Text
instance, since both types implement the Draw
trait.
Because it can host different concrete types at runtime, a trait object is "unsized", a.k.a. dynamically sized, which implies that it is only allowed to show up behind a reference or a smart pointer like std::boxed::Box
↗:
&dyn MyTrait
is a reference to a trait object (which can be anywhere, stack or heap),Box<dyn MyTrait>
is a heap-allocated trait object,Rc<dyn MyTrait>
or thread-safeArc<dyn MyTrait>
are used for shared ownership of trait objects.
Under the covers, a trait object pointer is a "fat pointer" consisting of two parts: a pointer to an instance of a type T
implementing SomeTrait
; and a 'vtable' (virtual method table) pointing to the specific implementation of each function in the SomeTrait
trait for the concrete type T
.
Trait objects permit "late binding" of methods. Calling a method on a trait object results in dynamic dispatch at runtime: that is, a function pointer is loaded from the trait object vtable and invoked indirectly. The actual implementation for each vtable entry can vary on an object-by-object basis.
The following example demonstrates the use of trait objects to store a heterogenous collection of UI widgets and dynamic dispatch:
//! Trait object example: a highly simplified GUI. /// A trait for types that can be drawn. trait Draw { /// Draws the object. fn draw(&self); } /// A struct representing a button. struct Button; /// Dummy implementation of the trait for `Button`. impl Draw for Button { fn draw(&self) { println!("Button"); } } /// A struct representing text to be drawn on a screen. struct Text; /// Dummy implementation. impl Draw for Text { fn draw(&self) { println!("Text"); } } /// A struct representing a screen that can display multiple components. struct Screen { /// A vector of trait objects that can be drawn. /// Note the `dyn` keyword. Trait objects are dynamically sized types. /// Like all DSTs, trait objects must be used behind some type of pointer - /// here, `Box`. /// /// We use a trait object here, because the `Screen` may hold a mix of /// `Button` and `Text` objects, which may be unknown until run-time. /// A generic type would not work here - it would allow only one type or /// the other, not a mix of both. components: Vec<Box<dyn Draw>>, // Heterogenous collection. } impl Screen { /// Creates a new screen with some components. The list below /// could be dynamically generated at run time. fn new() -> Self { Screen { components: vec![Box::new(Button), Box::new(Text), Box::new(Text)], } } /// Runs the screen, drawing each component. fn run(&self) { for component in self.components.iter() { // The purpose of trait objects is to permit "late binding" of // methods. Calling a method on a trait object results // in dynamic dispatch at runtime. component.draw(); } } } fn main() { let s = Screen::new(); s.run(); }
Note the following:
- Type information erasure: The concrete type is "erased" at compile time for the part of the code holding the trait object. The code only knows it has something that implements a trait.
- Object safety: Traits must be "dyn-compatible" (a.k.a. "object-safe") to be made into trait objects (see below).
Decide When to Use Trait Objects (and When Not To)
Use trait objects when:
- You need a heterogeneous collection of types that share a common interface:
// Cats and Dogs:
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
Box::new(Dog),
];
for animal in animals {
animal.make_noise(); // Dynamically dispatches to `Dog::make_noise` or `Cat::make_noise`, depending on the object, at run time.
}
- You want to return different concrete types implementing the same trait from a function, and the caller doesn't need to know the specific concrete type:
fn get_animal(is_dog: bool) -> Box<dyn Animal> {
if is_dog {
Box::new(Dog)
} else {
Box::new(Cat)
}
}
let my_dog = get_animal(true);
my_dog.make_noise();
- You are implementing a plugin system where the types of plugins are not known at compile time.
- You need to reduce compile times or binary size. Compared to generic types, trait objects can lead to smaller compiled binary sizes, because specialized (monomorphized) code is not generated for each concrete type; the trait-handling code is reused.
Consider alternatives (like generics or enums) when:
-
Performance is absolutely critical and the overhead of dynamic dispatch is unacceptable (profile first!). Dynamic dispatch incurs a small runtime overhead. As discussed above, there is an indirection when calling a method: first, the vtable pointer is dereferenced, then the method pointer within the vtable is dereferenced and called. This indirection can sometimes hinder compiler optimizations like inlining.
-
You have a small, fixed set of types that can implement the behavior. An enum with methods might be simpler and offer static dispatch:
enum Shape { Circle(CircleData), Square(SquareData), } impl Shape { fn draw(&self) { match self { Shape::Circle(data) => data.draw_circle(), Shape::Square(data) => data.draw_square(), } } }
Static Dispatch vs. Dynamic Dispatch
Trait objects enable dynamic dispatch. This means that the decision of which concrete method implementation to call is made at runtime, rather than at compile time. This is in contrast to static dispatch, which Rust uses with generics (and impl Trait
, see below), where the compiler generates specialized code for each concrete type used by a generic item.
Feature | Static Dispatch (Generics) | Dynamic Dispatch (Trait Objects) |
---|---|---|
Mechanism | Monomorphization (code duplication at compile time) | vtable lookups at runtime |
Performance | Generally faster (no runtime lookup overhead) | Slightly slower (due to vtable indirection) |
Flexibility | Less flexible for heterogeneous collections | More flexible for heterogeneous collections |
Code Size | Can lead to larger binaries (due to monomorphization) | Can lead to smaller binaries (no code duplication) |
Type Known | At compile time | At runtime (for the trait object itself) |
Syntax | fn foo<T: MyTrait>(item: T) | fn foo(item: &dyn MyTrait) |
impl Trait
vs dyn Trait
impl Trait
, where impl
is a keyword and Trait
is a trait name, specifies an unnamed but concrete type that implements a specific trait.
Both impl Trait
and dyn Trait
allow you to work with types that implement a particular trait, providing a form of abstraction or polymorphism. However, they differ significantly in how they achieve this: impl Trait
uses static dispatch (method calls are resolved at compile time); dyn Trait
uses dynamic dispatch (resolved at run time).
See Impl Trait for more details.
Trait Object Restrictions
Trait Objects Can Have Only One Base Trait
Trait objects can implement only one base trait. If you need a trait object for two or more traits, create a new trait that e.g. uses them as supertraits.
Note, however, two exceptions:
- Types that implement a trait must implement its supertraits. As a result, you can call supertrait methods on a trait object:
// Supertrait. trait Message { fn message(&self, msg: &str) -> String; } // Subtrait. trait Greet: Message { fn greet(&self) -> String { self.message("Hello.") } } // An example struct that implements `Greet`: struct Person { name: String, } impl Greet for Person {} // The struct must also implement the supertrait: impl Message for Person { fn message(&self, msg: &str) -> String { format!("{}: {msg}", self.name) } } // This function takes a trait object. fn say_hello_and_goodbye(entity: &dyn Greet) { println!("{}", entity.greet()); // Because `Greet` has `Message` as a supertrait, // we can also call `Message`'s methods. println!("{}", entity.message("Goodbye.")); } fn main() { let person = Person { name: String::from("Alice"), }; // You can directly pass a reference to `person`, // because it implements `Greet`. say_hello_and_goodbye(&person); // You can also explicitly create a trait object reference: let greet_obj: &dyn Greet = &person; say_hello_and_goodbye(greet_obj); }
Auto traits are special traits↗, one of std::marker::Send
↗, std::marker::Sync
↗, std::marker::Unpin
↗, std::panic::UnwindSafe
↗, and std::panic::RefUnwindSafe
↗. The compiler automatically implements these autotraits for types if certain conditions are met.
For example, the following are valid trait objects:
dyn Trait // Base trait.
dyn Trait + Send // Base + autotrait. The order does not matter.
dyn Trait + Send + Sync // Base + autotraits.
// Note: there may be also (at most one) lifetime parameter:
dyn Trait + 'static
dyn Trait + Send + 'static
Common examples include Send
and Sync
for thread safety. A type is Send
if it can be safely sent to another thread, and Sync
if it can be safely shared between threads. To use a trait object in a multithreaded environment, you will often need one or both of these autotraits:
// Define a trait with a method: trait Message { fn send_message(&self); } // Create a struct that will implement our trait. struct Worker { id: u32, msg: String, } // Implement the trait: impl Message for Worker { fn send_message(&self) { println!("Worker {}: {}", self.id, self.msg); } } // A function that takes a trait object with an autotrait: fn process_message(messenger: Box<dyn Message + Send>) { messenger.send_message(); } fn main() { let worker = Worker { id: 1, msg: "Hello".to_string(), }; // Create a boxed trait object. // The `Send` autotrait is automatically implemented for `Worker` // because all of its fields are `Send`. let messenger: Box<dyn Message + Send + 'static> = Box::new(worker); // Use the trait object in a separate thread. // This is possible because the trait object is `Send`. let handle = std::thread::spawn(move || { process_message(messenger); }); handle.join().unwrap(); }
Dyn Compatibility / Object Safety
Only a trait that is "dyn-compatible" (or "object-safe") can be made into a trait object. The rules for dyn compatibility are rather complicated, constraining both the trait and the methods within, and (as of June 2025) not consistently documented in the Rust reference↗. When designing a trait for use in a trait object, let the compiler's error messages guide you.
The dyn compatibility restrictions stem from the fact that trait objects are inherently !Sized
(dynamically sized types), because their concrete type (Self
) isn't known at compile time:
- The underlying trait cannot require
Self: Sized
. - All methods must not have
Self
as a return type. If a method returnsSelf
, the compiler wouldn't know the concrete size ofSelf
at runtime when dealing with a trait object. - All methods must not use generic type parameters. Generics are not compatible with vtables. If a method had a generic type parameter (e.g.,
fn foo<T>(&self, arg: T)
), the compiler couldn't fill in the concrete type forT
at runtime. - For the same reason, opaque return type (impl Trait,
async fn
) are not allowed.
In addition,
- The trait must not have any associated constant or any associated type with generics.
- All supertraits, if any, must also be dyn-compatible.
//! Example of (non-)dyn-compatible traits. use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; // Trait must not have any associated constant. trait NotObjectSafe { const CONST: usize = 10; } // Trait must not have any associated types with generics. trait NotObjectSafe2 { type Item<T>; } // `Sized` must not be a supertrait. trait NotObjectSafe3 where Self: Sized, { } trait NotObjectSafe4 { // Trait functions cannot have any type parameters // (although lifetime parameters are allowed). fn do_stuff<T>(&self, _t: T); } // Cannot use `Self` except in the type of the receiver. trait NotObjectSafe5 { fn clone_me(&self) -> Self; // Returns `Self`, not object-safe if `Self` is not `Sized`. } // Same problem: trait NotObjectSafe6 { fn clone_boxed(&self) -> Box<Self>; } // To make it object-safe, you might change `clone_me` // to return a `Box<dyn ObjectSafeClone>`: trait ObjectSafeClone { fn clone_boxed(&self) -> Box<dyn ObjectSafeClone>; } // Example struct. struct Circle { radius: f64, } impl ObjectSafeClone for Circle { fn clone_boxed(&self) -> Box<dyn ObjectSafeClone> { Box::new(Circle { radius: self.radius, }) } } // Additional examples of dyn-compatible methods. trait TraitMethods { // Trait methods must have a receiver with one of the following types: fn by_ref(&self) {} // Equivalent to self: &Self. fn by_ref_mut(&mut self) {} // Equivalent to self: &mut Self. fn by_box(self: Box<Self>) {} fn by_rc(self: Rc<Self>) {} fn by_arc(self: Arc<Self>) {} fn by_pin(self: Pin<&Self>) {} #[allow(clippy::needless_lifetimes)] fn with_lifetime<'a>(&'a self) {} // Equivalent to self: &'a Self. fn nested_pin(self: Pin<Arc<Self>>) {} // You can also constrain functions that otherwise could not be included in // a dyn-compatible trait, so they do not apply to trait objects. // They remain accessible on implementing concrete types. fn associated_function() where Self: Sized, { } // Same thing for methods that return `Self`: fn returns(&self) -> Self where Self: Sized; fn param(&self, other: Self) where Self: Sized, { } fn typed<T>(&self, x: T) where Self: Sized, { } } impl TraitMethods for Circle { fn returns(&self) -> Self where Self: Sized, { Circle { radius: self.radius, } } } fn main() { // These cause a compile error: // ERROR: the trait `NotObjectSafe` is not dyn compatible. // let obj: Box<dyn NotObjectSafe>; // let obj: Box<dyn NotObjectSafe2>; // let obj: Box<dyn NotObjectSafe3>; // let obj: Box<dyn NotObjectSafe4>; // let obj: Box<dyn NotObjectSafe5>; // let obj: Box<dyn NotObjectSafe6>; // But you can do: let obj: &dyn ObjectSafeClone = &Circle { radius: 1.0 }; let _cloned_obj: Box<dyn ObjectSafeClone> = obj.clone_boxed(); let mut obj: Box<dyn TraitMethods> = Box::new(Circle { radius: 1.0 }); obj.by_ref(); obj.by_ref_mut(); obj.by_box(); // Functions with a `where Self: Sized` constraints cannot be called on a // trait object... // obj.returns(); // ERROR: the `returns` method cannot be invoked on a trait object. // ...but can be called on an implementing type: let c = Circle { radius: 1.0 }; let _ = c.returns(); <Circle as TraitMethods>::associated_function(); }