Search code examples
rusttraitstrait-objects

Why using boxed objects over trait objects?


In the book Rust for Rustaceans, the author writes:

Broadly speaking, though, you’ll want to use static dispatch in your libraries and dynamic dispatch in your binaries. In a library, you want to allow your users to decide what kind of dispatch is best for them, since you don’t know what their needs are.

I guess that, in the binary case, he refers to this:

fn flexible_dispatch_method(_: &dyn MyTrait) {}

// static dispatch
//
let obj = MyType {};
flexible_dispatch_method(&obj);

// dynamic dispatch
//
let trait_obj: &dyn MyTrait = &MyType {};
flexible_dispatch_method(trait_obj);

Given the above, what's the advantage of using boxed objects instead of trait objects? Is it because of the need to use lifetimes:

fn return_new_trait_object<'a>() -> &'a dyn MyTrait {
    &MyType {}
}

or there is something else? Based on my understanding, in the dynamic dispatch case, the object needs to be allocated in the heap, anyway, so I presume there isn't much difference with a boxed object.


Solution

  • I think you might be misunderstanding a couple things here.

    • (Generally) static dispatch occurs whenever you call a method on a concrete type that's not dyn Trait. It's when the compiler decides which function to call at compile-time.
    • A trait object is anything that's some pointer to a dyn Trait, including Box<dyn Trait>, &dyn Trait, Arc<dyn Trait>, etc. When you call a function through a dyn Trait, the compiler inserts code to lookup the function to call at runtime, allowing for polymorphism and greater flexibility.

    In your code, flexible_dispatch_method(_: &dyn MyTrait) always uses dynamic dispatch, as signaled by the fact that its argument has the type &dyn MyTrait. An example of static dispatch would be the following:

    fn flexible_dispatch_method<T: MyTrait + ?Sized>(_: &T) {}
    

    With this declaration your first usage would use static dispatch and the second would use dynamic dispatch.

    Dynamic dispatch is a little more flexible since it avoids having generics all over the place. This can be useful for writing large applications where you might want to have polymorphism and easily add new implementations. However, dynamic dispatch has a performance cost, so that's why libraries should leave the dispatch choice up to the caller as much as possible.

    As per when to use &dyn Trait vs Box<dyn Trait>: it's all based on ownership. If you want an owned trait object use Box<dyn Trait>, and if you want a borrowed trait object use &dyn Trait.