What is the difference between impl Trait and dyn Trait

impl Trait uses static dispatch for known types at compile time, while dyn Trait uses dynamic dispatch for runtime polymorphism via trait objects.

The static vs dynamic wall

You are building a rendering engine. You have a Circle and a Square. Both implement a Draw trait. You write a function to render a shape. You try fn render(shape: impl Draw). It works. You pass a circle, you pass a square, the compiler is happy.

Then you try to store a list of shapes. You write Vec<impl Draw>. The compiler rejects you. You switch to Vec<&dyn Draw>. It compiles. You just hit the boundary between static dispatch and dynamic dispatch. The syntax looks similar, but the machine code is completely different. One approach generates specialized code for every type. The other uses a pointer to resolve behavior at runtime. Understanding the difference prevents performance leaks and unlocks flexible APIs.

How the compiler treats them differently

impl Trait is a promise to the compiler. You are saying, "I am passing a type that implements this trait, and you figure out which one." The compiler takes that promise and stamps out a dedicated version of the function for that specific type. This is static dispatch. The type is known at compile time. The generated code knows the exact size and layout of the data.

dyn Trait is a different promise. You are saying, "I am passing a box that contains some type implementing this trait, and I don't care which one." The compiler builds a single version of the function that works with a pointer to a table of function addresses. This is dynamic dispatch. The concrete type is erased. The function resolves method calls by jumping through a pointer at runtime.

Think of impl Trait like a custom-fitted glove. The factory makes a glove for your hand, and another for your friend's hand. Each glove fits perfectly. The factory needs to know whose hand it is making the glove for. dyn Trait is like a universal remote. You don't need a new remote for every TV. You point the remote at the TV and press "Volume Up". The remote sends a signal, and the TV figures out what to do. The remote works with any TV that speaks the protocol, but there is a tiny delay while the signal travels and the TV decodes it.

Minimal example

use std::fmt::Display;

/// Static dispatch: the compiler generates a unique function for each type.
/// No runtime overhead. The type is known at compile time.
fn print_static(item: impl Display) {
    println!("{}", item);
}

/// Dynamic dispatch: the function uses a pointer to resolve the method at runtime.
/// One function handles all types. Small runtime cost for flexibility.
fn print_dynamic(item: &dyn Display) {
    println!("{}", item);
}

fn main() {
    // Calls the i32 version of print_static
    print_static(42);

    // Calls the str version of print_static
    print_static("Rust");

    // Calls print_dynamic with a pointer to i32's Display impl
    print_dynamic(&42);

    // Calls print_dynamic with a pointer to str's Display impl
    print_dynamic(&"Rust");
}

Static dispatch: the stamp

When you call print_static(42), the compiler creates a function internally named something like print_static_i32. When you call print_static("Rust"), it creates print_static_str. These are separate functions in the binary. They are often inlined, meaning the code runs as fast as if you wrote the logic directly inside the call site.

This process is called monomorphization. The compiler duplicates the function body for every concrete type you use. If you have ten types implementing Display, you get ten versions of the function. This increases binary size slightly, but it eliminates runtime overhead. There are no pointer indirections. The CPU branch predictor can optimize the code flow.

Static dispatch also allows the compiler to optimize across the call boundary. If print_static is inlined, the compiler can see that item is an i32 and optimize the formatting logic specifically for integers. This is why impl Trait is the default choice for performance-critical code.

Use impl Trait when you want zero-cost abstraction and the caller passes a concrete type.

Dynamic dispatch: the pointer

When you call print_dynamic(&42), the compiler creates a single print_dynamic function. It receives a fat pointer. A fat pointer contains two parts: a pointer to the data and a pointer to a vtable. The vtable is a static table of function pointers generated by the compiler. It contains the address of i32's Display implementation.

Inside print_dynamic, the call to Display::fmt does not jump directly to the implementation. It looks up the function pointer in the vtable and jumps there. This indirection adds a small cost. The CPU has to dereference a pointer and potentially suffer a cache miss. In tight loops, this can matter. In most application code, the cost is negligible.

Dynamic dispatch enables type erasure. The function does not know the concrete type. It only knows that the data implements Display. This allows you to pass different types through the same interface without duplicating code.

Use dyn Trait behind a pointer when you need to store heterogeneous types in a collection or pass different types through the same interface.

The collection problem

The most common reason to reach for dyn Trait is collections. A Vec must contain elements of the same size and type. impl Trait represents a single concrete type. You cannot put a Circle and a Square in a Vec<impl Shape> because the compiler sees impl Shape as "some specific type", and Circle and Square are different types with different sizes.

trait Shape {
    fn area(&self) -> f64;
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { 3.14 * self.radius * self.radius }
}

impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.side }
}

/// This does not compile.
/// impl Shape is a single concrete type.
/// A Vec must contain elements of the same size and type.
// fn total_area(shapes: Vec<impl Shape>) -> f64 {
//     shapes.iter().map(|s| s.area()).sum()
// }

/// This compiles.
/// dyn Shape erases the concrete type.
/// All &dyn Shape pointers have the same size.
fn total_area(shapes: Vec<&dyn Shape>) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

The Vec<&dyn Shape> works because every element is a pointer. Pointers have a fixed size. The vtable handles the differences between Circle and Square at runtime. This is the escape hatch for heterogeneous data.

Trait objects are the escape hatch for heterogeneous data. Use them when you have to, not because they are convenient.

Why dyn needs a pointer

You cannot have a variable of type dyn Trait. The compiler rejects this with an error about unsized types. dyn Trait erases the size of the concrete type. The compiler needs to know the size to allocate memory on the stack. Since the size is unknown, you must use a pointer.

Box<dyn Trait> allocates on the heap and stores a pointer. &dyn Trait borrows the data and stores a reference. Rc<dyn Trait> and Arc<dyn Trait> add reference counting. The pointer carries the vtable pointer alongside the data pointer. This is why dyn always appears behind a pointer.

The Sized trait is a hidden bound on impl Trait. When you write impl Trait, the compiler assumes the type implements Sized. This means the type has a known size at compile time. dyn Trait is explicitly !Sized. This distinction explains why impl Trait works for values and dyn Trait requires indirection.

If you try to use a type that is not sized with impl Trait, the compiler rejects you. Use dyn Trait when size erasure is necessary.

Common traps

Return position impl Trait is a frequent source of confusion. In return position, impl Trait means "I return one specific type, but I am hiding the name." It does not mean "I return any type that implements the trait."

/// This fails with E0308.
/// impl Trait in return position means "I return one specific type".
/// The compiler sees two branches returning different types.
fn get_shape(flag: bool) -> impl Shape {
    if flag {
        Circle { radius: 1.0 }
    } else {
        Square { side: 1.0 }
    }
}

The compiler generates an error like mismatched types. It expects both branches to return the same concrete type. The fix is to use a trait object.

/// This works.
/// Box<dyn Shape> erases the type.
/// Both branches return a pointer to a Shape.
fn get_shape(flag: bool) -> Box<dyn Shape> {
    if flag {
        Box::new(Circle { radius: 1.0 })
    } else {
        Box::new(Square { side: 1.0 })
    }
}

Another trap is object safety. Not all traits can be used as dyn Trait. The trait must be object safe. Traits with generic methods or methods that take Self by value often fail object safety checks. The compiler rejects these with E0038. impl Trait does not have this restriction. You can use impl Trait with any trait, even if it is not object safe.

Object safety is a constraint of dynamic dispatch. If your trait is not object safe, you cannot use dyn Trait. You must stick to impl Trait or refactor the trait.

Convention asides

The dyn keyword is mandatory in modern Rust. Older versions of Rust allowed &Trait as shorthand for &dyn Trait. The language now requires the explicit keyword. This makes dynamic dispatch visible in the code. It signals to readers that there is a runtime cost. It also prevents ambiguity when a type name matches a trait name.

The community convention is to keep unsafe blocks small and to use dyn Trait only when necessary. If you can use impl Trait, use it. The performance difference is real, and static dispatch enables better compiler optimizations. When you do use dyn Trait, prefer &dyn Trait for borrowing and Box<dyn Trait> for ownership. Avoid Rc<dyn Trait> unless you need shared ownership without threads.

When to use which

Use impl Trait in function arguments when you want the compiler to generate optimized code for each concrete type and you do not care about the specific type name.

Use impl Trait in return types when the function always returns the same concrete type, but you want to hide that type from the caller.

Use dyn Trait behind a pointer when you need to store multiple different types in the same collection, like a Vec of shapes.

Use dyn Trait when you are building a plugin system or API where types are determined at runtime, not compile time.

Use Box<dyn Trait> when you need ownership of a trait object and the trait is object safe.

Use &dyn Trait when you only need to read or call methods without taking ownership.

Static dispatch is the default. Reach for dynamic dispatch only when you need type erasure.

Where to go next