3

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

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
dying_sphynx
  • 1,136
  • 8
  • 17

1 Answers1

4

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
};
trent
  • 25,033
  • 7
  • 51
  • 90
  • Great answer, thank you! I think for my current purposes dereferencing of the boxes should work fine. But it's nice to understand when to use Rc and Arc. The only thing I still don't understand is why my code with the reference to a Box doesn't compile... – dying_sphynx Mar 16 '19 at 17:19
  • 1
    @dying_sphynx I added some explanation. I think there is another question that goes into more detail on why `&T` -> `&dyn Trait` is a valid coercion but `&Box` -> `&Box` isn't; if I can find it I will link to it. – trent Mar 16 '19 at 17:30
  • 1
    One more interesting thing: I tried to inline calls to `Rc::clone` in your full example, but it didn't work (failed with the same problem of coercion of references you mention at the end). But when I used `m1.clone()` instead of `Rc::clone(&m1)` - it worked. That is unexpected, since I thought that `Rc::clone(&x)` and `x.clone()` should work the same: [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=51f4e673374091fa2409534bbc4478be) – dying_sphynx Mar 16 '19 at 17:57
  • 1
    @dying_sphynx I think that's because of the difference between [method auto-deref and coercion](https://stackoverflow.com/questions/53341819/what-is-the-relation-between-auto-dereferencing-and-deref-coercion) (in this case unsized coercion, not deref coercion). `m1.clone()` will always resolve to the type of `m1`, which is `Rc`. But `Rc::clone(&m1)` needs to infer the argument of `Rc<_>`, and it doesn't know that the coercion needs to happen "outside" the function call, so it picks the wrong thing (`Rc` instead of `Rc`). – trent Mar 16 '19 at 18:20
  • 1
    [Here's two other ways I found to resolve it.](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b5ac143ae7ae9a43c94d32dd8f35434e) – trent Mar 16 '19 at 18:21
  • 1
    I've tried your suggestions with references: `&*m1` works beautifully by creating a reference to the Box content, but it looks like just using `&m1` in this context doesn't work: [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=dab48d496bb4306deaea773a937ed76f) – dying_sphynx Mar 16 '19 at 20:42
  • 1
    @dying_sphynx You're right, thanks for the correction! I thought I had tried it but I must have made a mistake. – trent Mar 16 '19 at 20:56