Giving struct ownership of `dyn Trait`

I need to have a struct gain ownership of a dyn Trait field. How can I store any struct that implements Trait in my struct without creating any references? I really don't want the overhead of a Box.

you can only have single one unsized type as the last field of a struct, and this would make the struct unsized as well.

trait Trait {}
struct Foo {
    // other fields must be `Sized`
    x: i32,
    b: bool,
    // this must be the last field
    obj: dyn Trait,
}

note: working with a unsized type is very inconvinient, e.g. you cannot define a new() function that returns Self because the bound Self: Sized is not satisfied.


EDIT:

I should clarify that, if you define a DST directly like the example above, it's almost useless. you can define methods with &self and &mut self receivers, but you cannot create values of such types directly. (maybe there exists use cases with #[repr(C)] or ffi, but that'll be very rare I imagine).

a better way to define DSTs is to use generics, and the compiler will derive CoerceUnsized automatically for you so you can actually create (heap-allocated) values of the type. example:

trait Trait {}
impl Trait for () {}

struct Foo<T: ?Sized> {
    header: i32,
    payload: T,
}

impl<T> Foo<T> {
    fn new(header: i32, payload: T) -> Self {
        Foo { header, payload }
    }
}

fn main() {
    let foo = Foo::new(42, ());
    // coerce to reference
    let ref_dyn_foo: &Foo<dyn Trait> = &foo;
    // coerce to boxed value
    let box_dyn_foo: Box<Foo<dyn Trait>> = Box::new(foo);
}

you can define a constructor for boxed value for convinience

impl<T> Foo<T> {
    fn new_boxed_dyn(header: i32, payload: T) -> Box<Foo<dyn Trait>>
    where
        T: Trait + 'static,
    {
        Box::new(Self::new(header, payload))
    }
}
4 Likes

The dyn Trait mechanism requires that you have a pointer-indirection somewhere for technical reasons. That doesn’t necessarily need to be inside your struct, though:

use std::fmt::Display;
struct MyStruct<T:?Sized> {
    count:u32,
    extra:T
}

fn f(s:&MyStruct<dyn Display>) {
    let MyStruct { count, extra } = s;
    for _ in 0..*count {
        println!("{extra}");
    }
}

fn main() {
    f(&MyStruct { count:3, extra: "Hello" });
}
3 Likes

Note that the custom DSTs above don't have ownership. There's probably some max-size-based inline "box" approaches in the embedded world. But generally you would use something on the heap (Box, Arc, etc).

1 Like

If you want to pass the object by value, you're going to have to use an enum of all possible implementations instead of dyn Trait.

2 Likes

In addition to the other suggestions here, you might consider a crate like stack_dst.

I am confused by this claim. They certainly are responsible for dropping, and can potentially mutate, the T. What else are you defining ownership as?

1 Like

I suppose that was a terse and imprecise. For example:

    let foo = Foo::new(42, ());              // `Foo<()>` owns `()`
    let ref_dyn_foo: &Foo<dyn Trait> = &foo; // `&Foo<dyn Trait>`, no ownership
    // Creates temporary `MyStruct<&str>` that owns `&str`
    // Coerces to `&MyStruct<dyn Display>` with no ownership
    f(&MyStruct { count:3, extra: "Hello" });

In both cases the coercion from the owning, statically sized type to the dynamically sized type happens behind a non-owning reference. I don't think that's what the OP wanted.

But in the next statement in @nerditation’s version, you also have

let box_dyn_foo: Box<Foo<dyn Trait>> = Box::new(foo);

There is no sense in which the dyn Trait is not owned now.

My comment was aimed at the OP.

Ah, I see. Sorry for the long tangent.

My own perspective here is that boxing is probably necessary (if you don't go for the “inline box” approach which requires picking a maximum size), but custom DSTs may let you avoid having an additional indirection by using the same Box (or Arc or any other owning pointer) for another purpose in the application, so you're at least paying for only 1 indirection instead of 2.

I have personally found it useful to set up things like Arc<MyStruct<dyn Fn()>>, where MyStruct contains other fields that also need to be shared along with the caller-supplied Fn. So, there’s an allocation but it was an allocation I needed anyway.

3 Likes

That is a good point (and a nice pattern).

3 Likes