Introduction

When you need heterogeneous collections or runtime polymorphism, trait objects (dyn Trait) provide dynamic dispatch. Since Rust 1.86, you can also upcast trait objects to their supertraits, making dynamic dispatch even more flexible.

Key Concepts

  • Trait Object: A fat pointer (&dyn Trait or Box<dyn Trait>) that pairs a data pointer with a vtable pointer.
  • Vtable: A table of function pointers generated by the compiler for each concrete type implementing the trait.
  • Dyn Compatibility: The set of rules determining whether a trait can be used as a trait object (formerly called "object safety").
  • Trait Upcasting: Coercing &dyn SubTrait to &dyn SuperTrait, stabilized in Rust 1.86.

Real World Context

GUI frameworks store heterogeneous widget collections as Vec<Box<dyn Widget>>. Plugin systems use dyn Plugin to load implementations at runtime. Trait upcasting simplifies working with trait hierarchies and is especially useful with dyn Any.

Deep Dive

Trait objects let you mix different types behind a common interface:

rust
trait Animal {
    fn speak(&self) -> &str;
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn speak(&self) -> &str { "Woof!" }
}
impl Animal for Cat {
    fn speak(&self) -> &str { "Meow!" }
}

let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for a in &animals {
    println!("{}", a.speak()); // Dynamic dispatch via vtable
}

Each dyn Trait value is a fat pointer containing a pointer to the data and a pointer to the vtable. The vtable holds the drop function, size, alignment, and pointers to each trait method.

Trait Upcasting (Rust 1.86+)

You can now coerce a trait object to any of its supertraits:

rust
use std::any::Any;
use std::fmt::Debug;

trait MyTrait: Debug + Any {
    fn name(&self) -> &str;
}

fn print_debug(obj: &dyn MyTrait) {
    let debug: &dyn Debug = obj; // Upcast to supertrait
    println!("{:?}", debug);
    let any: &dyn Any = obj; // Upcast to Any
    if let Some(s) = any.downcast_ref::<String>() {
        println!("It's a string: {s}");
    }
}

Dyn Compatibility Rules

Not all traits can be used as trait objects. A trait is dyn-compatible when:

  1. It has no Self: Sized supertrait bound
  2. It has no associated constants
  3. All methods have a self receiver (&self, &mut self, Box<Self>, etc.)
  4. No method returns Self (without a where Self: Sized escape hatch)
  5. No method has generic type parameters
  6. No method returns -> impl Trait or is async fn (these are not dyn-compatible)
rust
// Dyn-compatible
trait Drawable { fn draw(&self); }

// NOT dyn-compatible: returns Self
trait Duplicatable { fn duplicate(&self) -> Self; }

// NOT dyn-compatible: generic method
trait Processor { fn process<T>(&self, val: T); }

Common Pitfalls

  1. Forgetting dyn compatibility rules — Adding a generic method or associated constant to a trait makes it unusable as dyn Trait. Use where Self: Sized on non-dispatchable methods to keep the trait dyn-compatible.
  2. Unnecessary boxing — Using Box<dyn Trait> when &dyn Trait suffices wastes a heap allocation.

Best Practices

  1. Default to static dispatch — Use generics unless you specifically need heterogeneous collections or runtime polymorphism.
  2. Use trait upcasting with Any for type erasure patterns — Since Rust 1.86, you can upcast to dyn Any and downcast to recover the concrete type.

Summary

  • dyn Trait enables runtime polymorphism via fat pointers and vtables.
  • Trait upcasting (Rust 1.86) lets you coerce &dyn SubTrait to &dyn SuperTrait.
  • Dyn compatibility rules restrict which traits can be used as trait objects.
  • async fn and -> impl Trait methods are not dyn-compatible.

Code Examples

rust
// Different ways to hold trait objects
trait Draw {
    fn draw(&self);
}

struct Circle;
impl Draw for Circle { fn draw(&self) { /* ... */ } }

// Owned trait object (heap-allocated)
let shape: Box<dyn Draw> = Box::new(Circle);

// Borrowed trait object
let shape_ref: &dyn Draw = &Circle;

// Shared ownership trait object
use std::sync::Arc;
let shape_shared: Arc<dyn Draw> = Arc::new(Circle);

// Factory pattern returning trait objects
fn create_shape(kind: &str) -> Box<dyn Draw> {
    match kind {
        "circle" => Box::new(Circle),
        _ => Box::new(Circle),
    }
}
✓ Completed