Trait Objects and Dynamic Dispatch

Returning Trait Objects

When a function must return different concrete types across conditional branches that implement a common trait, returning a boxed trait object (-> Box<dyn Trait>) erases the concrete return type behind a uniform 16-byte pointer.

Why Conditional Branches Require Box<dyn Trait> (Not impl Trait)

An opaque return type (-> impl Trait) is resolved at compile time to a single concrete type, so returning two different structs from if / else or match arms fails with E0308. Returning Box<dyn Trait> unifies all branches into the same return type:

trait Exporter {
    fn export(&self, rows: &[&str]) -> String;
}

struct CsvExporter;

impl Exporter for CsvExporter {
    fn export(&self, rows: &[&str]) -> String {
        rows.join(",")
    }
}

struct JsonArrayExporter;

impl Exporter for JsonArrayExporter {
    fn export(&self, rows: &[&str]) -> String {
        let quoted: Vec<String> = rows.iter().map(|r| format!("\"{r}\"")).collect();
        format!("[{}]", quoted.join(", "))
    }
}

// Factory function selecting a concrete implementation at runtime:
fn create_exporter(format: &str) -> Result<Box<dyn Exporter>, String> {
    match format {
        "csv" => Ok(Box::new(CsvExporter)),
        "json" => Ok(Box::new(JsonArrayExporter)),
        other => Err(format!("unsupported format: {other}")),
    }
}

fn main() {
    let data = ["rust", "systems", "2024"];

    for fmt in ["csv", "json"] {
        let exporter = create_exporter(fmt).unwrap();
        println!("{fmt:<4} -> {}", exporter.export(&data));
    }
}
csv  -> rust,systems,2024
json -> ["rust", "systems", "2024"]

Returning Trait Objects That Borrow Input Data (Box<dyn Trait + 'a>)

By default, -> Box<dyn Trait> is shorthand for -> Box<dyn Trait + 'static>. If the returned trait object wraps a struct or iterator that borrows from an input parameter with lifetime 'a, annotate the trait object's lifetime bound as Box<dyn Trait + 'a> (or Box<dyn Trait + '_>):

fn filter_numbers<'a>(
    slice: &'a [i32],
    only_positive: bool,
) -> Box<dyn Iterator<Item = i32> + 'a> {
    if only_positive {
        Box::new(slice.iter().copied().filter(|&x| x > 0))
    } else {
        Box::new(slice.iter().copied())
    }
}

fn main() {
    let nums = [-5, 10, -2, 20];

    let pos: Vec<i32> = filter_numbers(&nums, true).collect();
    let all: Vec<i32> = filter_numbers(&nums, false).collect();

    println!("Positive only: {pos:?}");
    println!("All numbers:   {all:?}");
}
Positive only: [10, 20]
All numbers:   [-5, 10, -2, 20]

-> impl Trait vs. -> Box<dyn Trait> Decision Guide

Requirement Use -> impl Trait Use -> Box<dyn Trait>
Function returns a single concrete type Yes (zero heap allocation, inlinable) Unnecessary overhead
Function returns different types in if / else or match No (E0308) Yes
Trait is not dyn-compatible (e.g. Clone) Yes No (E0038)