Search code examples
rusttraitstrait-objects

How to share heap-allocated trait objects?


I have a trait and a struct implementing that trait (a trait object). I'd like to allocate my trait objects on the heap and to have other structures refer to them.

Box field

trait Material {}

struct Iron {}

impl Material for Iron {}

// It works, but takes ownership of boxes.
struct Sphere {
    radius: f64,
    material: Box<dyn Material>,
}

This code works but I can't have two spheres sharing the same Material, because the Box owns the material and a sphere owns its Box field.

Reference field

My next attempt is to use a normal reference instead of a Box:

struct Sphere<'a> {
    radius: f64,
    material: &'a dyn Material,
}

This also works, but as far as I understand, my Materials will be allocated on the stack instead of the heap. What if the Material value is really big and I'd rather have it on the heap? This leads me to the next approach which does not compile:

Reference to a Box

struct Sphere<'a> {
    radius: f64,
    material: &'a Box<dyn Material>,
}

fn main() {
    let m1 = &Box::new(Iron {});
    let s1 = Sphere {
        radius: 1.0,
        material: m1,
    };
    assert_eq!(s1.radius, 1.0);
}

This gives me the following error:

error[E0308]: mismatched types
  --> src/main.rs:16:19
   |
16 |         material: m1,
   |                   ^^ expected trait Material, found struct `Iron`
   |
   = note: expected type `&std::boxed::Box<(dyn Material + 'static)>`
              found type `&std::boxed::Box<Iron>`

I am not quite sure where 'static comes from in that type and it looks like it confuses the type checker. Otherwise dyn Material and Iron can be unified as far as I can understand.

Playground


Solution

  • Rc or Arc

    When you need shared ownership, Rc or Arc is usually the first tool to reach for. These types implement sharing by reference counting, so cloning one is cheap (just copy a pointer and increment the refcount). Either works handily in this case:

    struct Sphere {
        radius: f64,
        material: Rc<dyn Material>,
    }
    
    let m1 = Rc::new(Iron {});
    let s1 = Sphere {
        radius: 1.0,
        material: m1,
    };
    

    m1 is of the concrete type Rc<Iron>, but because it implements the CoerceUnsized trait, it can be automatically coerced in contexts that expect an Rc<dyn Material>. You can make multiple Spheres refer to the same material by cloneing m1. (Full example)

    The difference between Rc and Arc is that Arc is safe to use for sharing between multiple threads, but Rc is not. (Also see When to use Rc vs Box?)

    References

    As for your reference example:

    This also works, but as far as I understand, my Materials will be allocated on the stack instead of the heap.

    It's true that lifetimes are derived from the stack, but the reference itself does not need to point to something on the stack. For example, you can take a reference to the T in a Box<T> by dereferencing the Box:

    struct Sphere<'a> {
        radius: f64,
        material: &'a dyn Material,
    }
    
    let m1 = Box::new(Iron {});
    let s1 = Sphere {
        radius: 1.0,
        material: &*m1, // dereference the `Box` and get a reference to the inside
    };
    let s2 = Sphere {
        radius: 2.0,
        material: &*m1,
    };
    

    This is even cheaper than using Rc because & references are Copyable, but even though the Iron itself is stored on the heap, the references that point to it are still bound to the lifetime of the stack variable m1. If you can't make m1 live long enough, you'll probably want to use Rc instead of plain references.

    Reference to a Box

    This approach should also work, although it is unnecessary. The reason it doesn't is because, although you can coerce a Box<Iron> to a Box<dyn Material>, you can't coerce a &Box<Iron> to a &Box<dyn Material>; the types are incompatible. Instead, you need to create a stack variable of type Box<dyn Material> so that you can take references to it.

    let m1: Box<dyn Material> = Box::new(Iron {}); // coercion happens here
    let s1 = Sphere {
        radius: 1.0,
        material: &m1,  // so that this reference is the right type
    };