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.
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.
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 Material
s 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:
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.
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 Sphere
s refer to the same material by clone
ing 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?)
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 Copy
able, 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.
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
};