Trait Objects and Dynamic Dispatch

Trait Objects Behind References

Using borrowed trait objects (&dyn Trait for shared access and &mut dyn Trait for mutable access) provides runtime polymorphism without any heap allocation.

Zero-Allocation Dynamic Dispatch with &dyn Trait

A common misconception is that dynamic dispatch in Rust always requires allocating a Box on the heap. When a function only needs to borrow a polymorphic value for the duration of the call, accepting &dyn Trait or &mut dyn Trait constructs the 16-byte fat pointer directly on the stack pointing to the caller's stack-allocated value:

Caller Stack Frame                  Callee (`process(w: &mut dyn Writer)`)
+-------------------------+         +------------------------+
| buf: StackBuffer        | <────── | w.data_ptr             | (No heap allocation!)
+-------------------------+         | w.vtable_ptr           |
                                    +------------------------+
trait Sink {
    fn write_record(&mut self, record: &str);
}

struct ConsoleSink;

impl Sink for ConsoleSink {
    fn write_record(&mut self, record: &str) {
        println!("[CONSOLE] {record}");
    }
}

struct VecSink<'a> {
    storage: &'a mut Vec<String>,
}

impl Sink for VecSink<'_> {
    fn write_record(&mut self, record: &str) {
        self.storage.push(record.to_owned());
    }
}

// Accepts `&mut dyn Sink` — no `Box` allocation required:
fn emit_startup_events(sink: &mut dyn Sink) {
    sink.write_record("init_config");
    sink.write_record("bind_socket");
}

fn main() {
    let mut console = ConsoleSink;
    emit_startup_events(&mut console);

    let mut buffer = Vec::new();
    let mut vec_sink = VecSink {
        storage: &mut buffer,
    };
    emit_startup_events(&mut vec_sink);

    println!("Buffered records: {buffer:?}");
}
[CONSOLE] init_config
[CONSOLE] bind_socket
Buffered records: ["init_config", "bind_socket"]

Conditional Stack Selection Without Box

You can also bind a local &dyn Trait reference across if / else branches when the concrete values live on the same stack frame:

use std::fmt::Display;

fn main() {
    let use_hex = false;
    let count = 42_u32;
    let label = "forty-two";

    // Both branches borrow stack values as `&dyn Display` without heap allocation:
    let printable: &dyn Display = if use_hex { &label } else { &count };

    println!("Selected value: {printable}");
}
Selected value: 42

Lifetime Rules for &'a (dyn Trait + 'b)

When a struct S<'b> containing a lifetime 'b is borrowed as &'a dyn Trait, the compiler requires 'b: 'a (the data inside the struct must live at least as long as the reference &'a to the trait object). By default, &'a dyn Trait elides to &'a (dyn Trait + 'a), which satisfies this automatically.