Course contentsShow
← Rust
Lesson 393 of 1,9104. Lifetimes and Advanced OwnershipFree lesson

Dynamic Dispatch with Lifetimes

Balance dynamic dispatch overhead with the strict lifetime requirements of trait objects.

In this lesson, you will learn how to balance the performance overhead of dynamic dispatch with the strict compiler requirements imposed by object lifetimes.

The Conflict: Erasure vs. Safety

When we use dynamic dispatch via trait objects like &dyn Trait, Rust performs type erasure. At runtime, the compiler loses the concrete type information (like i32 or MyStruct) and relies on a vtable to execute methods. However, the lifetime of that reference is not erased. If your trait object holds a reference to data, the compiler must ensure that the object doesn't outlive the data it points to.

The Lifetime Bound Requirement

By default, a trait object &dyn Trait assumes a 'static lifetime if not specified, which is often too restrictive. To allow for borrowed data, we use explicit lifetime annotations. This tells the compiler: "This trait object is valid as long as this specific lifetime remains valid."

Consider a logger that holds a reference to a configuration:

trait Logger {
    fn log(&self, msg: &str);
}

struct ConsoleLogger<'a> {
    prefix: &'a str,
}

impl<'a> Logger for ConsoleLogger<'a> {
    fn log(&self, msg: &str) {
        println!("{}: {}", self.prefix, msg);
    }
}

// We constrain the trait object to the lifetime of the input
fn process<'a>(logger: &'a dyn Logger) {
    logger.log("Processing data...");
}

Balancing Overhead

Dynamic dispatch introduces a minor runtime cost because of pointer indirection (the vtable lookup). When you add lifetime bounds, you are essentially narrowing the scope of where that object can be used.

If you find yourself passing around &'a dyn Trait, you are effectively "pinning" the dynamic object to a specific scope. This is safer than 'static but can lead to "lifetime leaking" where a long-lived component cannot store the object because the trait object's lifetime is too short. To resolve this, ensure that the trait object's lifetime bound is at least as long as the component intending to store or use it. If the performance impact of this indirection matters, consider if your trait objects are being created inside hot loops—in such cases, opting for generics (static dispatch) is usually preferred unless you specifically need runtime polymorphism.

Key Takeaway: When using &dyn Trait with references, you must explicitly annotate lifetimes to ensure the trait object never outlives the data it depends on, preventing dangling pointers at the cost of narrower scope flexibility.