rusttraitstype-traitstrait-objects

Why are trait objects usually used via references (&dyn Trait) or smart Pointers (like Box<dyn Trait>)?


In Rust, why are trait objects usually used via references (&dyn Trait) or smart Pointers (like Box<dyn Trait>)? Does it have to be? Or is it better to use it this way?


Solution

  • They are handled via references and smart pointers because they won't compile otherwise:

    trait Foo {}
    struct Bar;
    impl Foo for Bar {}
    
    fn main() {
        let f1: dyn Foo = Bar;
        let f2: dyn Foo = Bar as dyn Foo;
    }
    
    error[E0308]: mismatched types
     --> src/main.rs:6:23
      |
    6 |     let f1: dyn Foo = Bar;
      |             -------   ^^^ expected `dyn Foo`, found `Bar`
      |             |
      |             expected due to this
      |
      = note: expected trait object `dyn Foo`
                       found struct `Bar`
      = help: `Bar` implements `Foo` so you could box the found value and coerce it to the trait object `Box<dyn Foo>`, you will have to change the expected type as well
    
    error[E0620]: cast to unsized type: `Bar` as `dyn Foo`
     --> src/main.rs:7:23
      |
    7 |     let f2: dyn Foo = Bar as dyn Foo;
      |                       ^^^^^^^^^^^^^^
      |
    help: consider using a box or reference as appropriate
     --> src/main.rs:7:23
      |
    7 |     let f2: dyn Foo = Bar as dyn Foo;
      |                       ^^^
    
    error[E0277]: the size for values of type `dyn Foo` cannot be known at compilation time
     --> src/main.rs:6:9
      |
    6 |     let f1: dyn Foo = Bar;
      |         ^^ doesn't have a size known at compile-time
      |
      = help: the trait `Sized` is not implemented for `dyn Foo`
      = note: all local variables must have a statically known size
      = help: unsized locals are gated as an unstable feature
    help: consider borrowing here
      |
    6 |     let f1: &dyn Foo = Bar;
      |             +
    

    A dyn Trait is a dynamic type that represents any potential concrete type that implements that Trait. Since it is representing a variety of types of potentially different sizes, it itself does not have a fixed size. This is called a dynamically sized type (DST).

    Because they don't have a fixed size, there are limits to where we can put them. In particular a DST cannot be stored in a variable; and if stored in a struct, it makes that struct a DST which therefore also cannot be stored in a variable. There is a unsized locals RFC that may make it possible in the future, but right now its fate is unclear.

    Side note: you can still reference a variable as a dynamic trait but that is because the variable is still a known concrete type even though the trait object accesses it dynamically:

    fn main() {
        let b = Bar;
        let f: &dyn Foo = &b;
    }
    

    So in order to work with a DST, it requires some indirection; the value must be stored elsewhere and only referred to through a reference or smart pointer like Box (Rc, Arc, also work).


    Technically tangential but worth mentioning anyway: references and smart pointers to trait objects are "fat". This means they actually store two pointers: one to the data and another to a v-table that maps to the trait functions associated with that trait implementation. See What is a "fat pointer"? for more info.