Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

A trait object is a dynamically sized type (DST) that enables late binding (dynamic dispatch) in Rust. It allows a pointer to abstract over different concrete types at runtime, provided those types implement a specific trait. Because the compiler cannot know the size of the underlying concrete type at compile time, trait objects must always be accessed through a pointer, such as a reference (&dyn Trait) or a smart pointer (Box<dyn Trait>).

Syntax

The dyn keyword explicitly denotes a trait object, distinguishing dynamic dispatch from static dispatch (which uses impl Trait or generic bounds).
trait Renderable {
    fn render(&self);
}

// Trait object behind a shared reference
let ref_obj: &dyn Renderable;

// Trait object behind a mutable reference
let mut_ref_obj: &mut dyn Renderable;

// Trait object behind an owned smart pointer
let boxed_obj: Box<dyn Renderable>;

Memory Layout: Fat Pointers

Under the hood, a pointer to a trait object is a fat pointer. It is twice the size of a standard machine pointer (e.g., 16 bytes on a 64-bit architecture) because it consists of two distinct memory addresses:
  1. Data Pointer: A pointer to the actual instance of the concrete type allocated in memory.
  2. vtable Pointer: A pointer to a Virtual Method Table (vtable) specific to the concrete type.
The vtable is generated by the compiler at compile time for each type implementing the trait. It contains:
  • Function pointers to the concrete implementations of the trait’s methods.
  • The size of the concrete type.
  • The memory alignment requirements of the concrete type.
  • A pointer to the concrete type’s drop implementation.

Dynamic Dispatch Mechanism

When a method is invoked on a trait object, the compiler cannot inline the function call or resolve it directly. Instead, it performs dynamic dispatch:
  1. It dereferences the vtable pointer to locate the vtable for the underlying type.
  2. It looks up the memory address of the specific method within that vtable.
  3. It invokes the method, passing the data pointer as the self argument.
This indirection prevents monomorphization (code bloat) but introduces a slight runtime cost due to the pointer dereferencing and the inability of the compiler to inline the method.

Object Safety Rules

Not all traits can be coerced into trait objects. A trait must be object-safe to be used with the dyn keyword. The compiler enforces strict rules to ensure a valid vtable can be constructed and that the type can be dynamically sized. A trait is object-safe if it meets the following criteria:
  1. The trait must not require Self: Sized: The trait declaration cannot have a Sized supertrait bound (e.g., trait MyTrait: Sized {}). Because a trait object (dyn Trait) is a dynamically sized type, it is inherently !Sized and cannot implement a trait that mandates Sized implementors.
  2. No Self in method signatures (except the receiver): Methods cannot use the Self type anywhere in their signature except as the receiver itself (self, &self, &mut self, Box<Self>, etc.). Self cannot be used as a return type, nor can it be used as the type of any other parameter (e.g., fn eq(&self, other: &Self)). If Self is used outside the receiver, the compiler cannot guarantee type safety or determine the concrete type’s size at runtime.
  3. No generic type parameters on methods: Methods cannot have generic type parameters. Generics require monomorphization (generating a unique function per type), which would result in an infinitely sized vtable.
  4. Methods must have a receiver: Methods must take some form of self. Associated functions without a receiver parameter (often used as constructors) cannot be called on a trait object because there is no instance data to determine which concrete implementation’s vtable to invoke.
// ✅ Object-safe trait
trait Processor {
    fn process(&self, data: &[u8]) -> usize;
    fn reset(&mut self);
}

// ❌ Non-object-safe traits
trait SizedTrait: Sized { // Violates Rule 1: Requires Self: Sized
    fn process(&self);
}

trait Factory {
    fn new() -> Self; // Violates Rule 2 & 4: Returns Self, no receiver
    
    fn compare(&self, other: &Self); // Violates Rule 2: Self used as a non-receiver parameter
    
    fn transform<T>(&self, input: T); // Violates Rule 3: Generic type parameter 'T'
}
If a trait contains non-object-safe methods, you can explicitly exclude those specific methods from the trait object by adding a where Self: Sized bound to them. This exempts the method from the vtable, allowing the rest of the trait to remain object-safe.
trait PartiallySafe {
    // Object-safe method (included in vtable)
    fn execute(&self);

    // Excluded from trait objects, preserving object safety for `execute`
    fn create() -> Self where Self: Sized;
}
Master Rust with Deep Grasping Methodology!Learn More