Sling Academy
Home/Rust/Rust - Understanding the differences between `Box<dyn Trait>` and `impl Trait`

Rust - Understanding the differences between `Box` and `impl Trait`

Last updated: January 04, 2025

In Rust, handling polymorphism and abstractions can significantly impact the design and flexibility of your application. Two common ways to implement trait abstractions are using Box<dyn Trait> and impl Trait. Although they might seem similar at first glance due to both dealing with traits, they serve different purposes and have some fundamental differences. Understanding these differences can help you make better decisions when designing your Rust applications.

What is Box<dyn Trait>?

Box<dyn Trait> is a type of trait object. Trait objects let you use dynamic dispatch to call methods on a trait, while the concrete type implementing the trait is not explicitly known at compile time. This is very useful when you need polymorphism and the specific type is determined at runtime.

trait Drawable {
    fn draw(&self);
}


struct Circle;

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle");
    }
}


fn draw_shape(shape: Box<dyn Drawable>) {
    shape.draw();
}


fn main() {
    let circle = Box::new(Circle);
    draw_shape(circle);
}

Here, draw_shape function can accept any type that implements Drawable trait, but it requires boxing, i.e., wrapping into Box<dyn Drawable>. This has the overhead of heap allocation, but it is flexible because the exact type need not be known at compile time.

What is impl Trait?

The impl Trait syntax was introduced to provide a way to specify that a function returns some type that implements a given trait without specifying the concrete type. This is also known as opaque types.

trait Flyable {
    fn fly(&self);
}


struct Bird;

impl Flyable for Bird {
    fn fly(&self) {
        println!("Flying like a bird");
    }
}


fn get_flyable() -> impl Flyable {
    Bird
}


fn main() {
    let flying_creature = get_flyable();
    flying_creature.fly();
}

In the example above, get_flyable returns some type that implements the Flyable trait using impl Flyable, without specifying what type it is outside the function. Here, the exact type does need to be consistent across all calls and known within the function returning it, maintaining the compile-time type safety and often optimizing away any dynamic dispatch overhead.

Differences Between Box<dyn Trait> and impl Trait

  • Type Erasure: With Box<dyn Trait>, the type is erased, meaning you don't need to know the concrete type, allowing runtime polymorphism.
  • Interface Stability: Box<dyn Trait> interfaces can evolve without impacting users who compile against a binary. The dispatch is dynamic here due to vtables.
  • Heap Allocation: Using Box<dyn Trait>, the object needs to be moved to the heap, which can incur performance costs due to allocation/deallocation.
  • Compile-time Type: impl Trait retains compile-time information of a concrete but opaque type, hence offering optimizations borrowed from knowing the concrete type.
  • Generic Functions: impl Trait is useful in defining return types for generic functions without including explicit type parameters.

When to Use Which

If your use case requires passing potentially self-referential structured data or requires storing heterogeneous collections of types that implement the same trait, Box<dyn Trait> can be very valuable. In contrast, use impl Trait when you want to leverage zero-cost abstractions and maintain type information as far as possible while coding, making the static dispatch.

Choosing between Box<dyn Trait> and impl Trait fundamentally revolves around your specific needs for dynamic versus static dispatch and explicit versus implicit type management in your Rust programs.

Next Article: Why Rust requires explicit trait bounds instead of inheritance

Previous Article: Rust - Balancing compile times vs code bloat in heavily generic code

Series: Generic types in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior