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-safe Arc<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.

FeatureStatic Dispatch (Generics)Dynamic Dispatch (Trait Objects)
MechanismMonomorphization (code duplication at compile time)vtable lookups at runtime
PerformanceGenerally faster (no runtime lookup overhead)Slightly slower (due to vtable indirection)
FlexibilityLess flexible for heterogeneous collectionsMore flexible for heterogeneous collections
Code SizeCan lead to larger binaries (due to monomorphization)Can lead to smaller binaries (no code duplication)
Type KnownAt compile timeAt runtime (for the trait object itself)
Syntaxfn 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);
}
  • Trait objects can include "auto traits".

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 returns Self, the compiler wouldn't know the concrete size of Self 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 for T 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();
}