Vector of generic trait objects

pub trait IO {}
impl IO for f64 {}

pub trait Unit<Input: IO, Output: IO> {
    fn process(&self, t: Option<Input>) -> Option<Output>;
}

pub struct UnitA;
impl Unit<f64, f64> for UnitA {
    fn process(&self, t: Option<f64>) -> Option<f64> {
        t /* this is just so compilator won't complain */
    }
}
impl UnitA {
    pub fn new() -> UnitA {
        UnitA {   }
    }
}

pub struct UnitB;
impl Unit<f64, f64> for UnitB {
    fn process(&self, t: Option<f64>) -> Option<f64> {
        t /* this is just so compilator won't complain */
    }
}
impl UnitB {
    pub fn new() -> UnitB {
        UnitB {   }
    }
}

fn main() {
   /* trial example */
    let units: Vec<Box<dyn Unit<dyn IO, dyn IO>>> = vec![
        Box::new(UnitA::new()), /* <= this does not work */
        Box::new(UnitB::new()), /* <= this does not work */
    ];
}

Hello everyone :slight_smile:

(link to playground with exact same code)

I am trying to understand exactly how trait objects and generics work together, but I feel like I am missing a major piece. About 'dyn' exactly.

In this example, I am trying to build a Vec of trait objects that are safe objects, using different types of struct (UnitA, UnitB) that both implement the same generic trait object (Unit<IO, IO>).

As UnitA or UnitB would fit the dyn Unit, I naively thought impl IO for f64 would have fit the dyn IO generic requirement. But compiler isn't content:

  --> src/main.rs:36:9
   |
36 |         Box::new(UnitB::new()),
   |         ^^^^^^^^^^^^^^^^^^^^^^ the trait `Unit<dyn IO, dyn IO>` is not implemented for `UnitB`
   |
   = help: the following implementations were found:
             <UnitB as Unit<f64, f64>>

I understand this error, but I am not sure how to satisfy the compiler about the dyn IO requirement.
What should I do differently for the compiler to be happy?
Does that mean that only concrete type, in place of dyn IO, must be passed to Unit<> of the Vec declaration for the compiler to be able to build a functional vector?
Thanks a lot for your time!

I feel one of the key things to understand about dyn Trait is that it is not a dynamic type. It is a static, concrete type. It performs dynamic dispatch, and it has dynamic size (aka it is unsized), but it is a static type.

It's dynamically sized because base types of different sizes can implement the Trait, and when they're coerced into dyn Traits, you can have dyn Trait of different sizes.

Because the size is unknown from the type alone, you typically see dyn Trait behind some sort of pointer -- Box<dyn Trait> or &dyn Trait. The pointers here are actually wide pointers -- there is a pointer to the value, and there is another pointer to a vtable for the erased type. The vtable contains the size of the value.

The next thing to know is how to work with unsized types when using generics. Most types have a static size, and they automatically implement the Sized trait. Aside from dyn Trait, the other main unsized types in Rust are various types of slices -- [u8], str, Path and so on. These do not implement the Sized trait.

Most places where you introduce generics have an implicit Sized bound, because it's usually what you want. Rust can't currently deal with moving unsized values around, and most data structures can't handle unsized types either. If you want to handle unsized types in generics, you usually have to remove the implicit bound using ?Sized: fn foo<T: ?Sized>(_: &T) { /* ... */ }

And one more thing about dyn Trait: The compiler supplies an implementation of Trait for dyn Trait, but it does not supply one for &dyn Trait or Box<dyn Trait>, etc. However, you can supply your own.


So, in your code. First, here

impl Unit<f64, f64> for UnitA

You're not implementing Unit<dyn IO, dyn IO> here. dyn IO is it's own, concrete type! You're implementing Unit<f64, f64> for UnitA, and nothing more.

But if you tried this:

impl Unit<dyn IO, dyn IO> for UnitA {
    fn process(&self, t: Option<dyn IO>) -> Option<dyn IO> {
        t
    }
}

It complains that the generics of the parameters of Unit must be Sized (due to that implicit bound). And then if you try to allow unsized types:

pub trait Unit<Input: IO + ?Sized, Output: IO + ?Sized> {
    fn process(&self, t: Option<Input>) -> Option<Output>;
}

You'll find that Option<_> doesn't support unsized types.


Can we get something close to what you wanted, where everything is type-erased? It could look something like

Vec<Box<dyn Unit<Box<dyn IO>, Box<dyn IO>>>>

But for that to work, we will need

  • Box<dyn IO> to implement IO
  • UnitA and UnitB to implement Unit<Box<dyn IO>, Box<dyn IO>>

Here's a version of the playground that does that.

4 Likes

Your concise explanation about dyn Trait definitely should be the material a beginner (like me) is familiar with when he/she comes across dyn Trait. :heart:
And that explanation is a half summary of this article by niko[1].


  1. The other half is about the origin of dyn safety (or object safety), which is not related to OP though. ↩ī¸Ž

1 Like

Thanks @quinedot, a lot, for this complete, detailed answer. I learned a lot, especially the bit about dyn being a static concrete type while providing possibility for dynamic traits. That helped to clear a good chunk of fog! As @vague concretely said, this could surely be teaching material for rust beginners <3

Thanks for the sample of code! I've been studying it for a while, and there is still one thing I could not get my head around.

My first intention was for this Vec<Box<dyn Unit<Box<dyn IO>, Box<dyn IO>>> Vector to store different kind of type that would allow process to take, for example, an Option<f64> as parameter and return Option<String>. And I could not find how to do this with the implementation you kindly gave.

After fiddling a bit more with this intention in mind, I started to conclude that the problem was me using Rust improperly and not getting into the right philosophy.
I should not try to make everything looks like interface implementation fiesta and use Rust the proper way. Make more use of concrete static type, or enums when needing to handle multiple types in a single situation (which I managed to do in a much much simpler way, to fulfill my first intention described just before), or pattern that makes use of enums/static types in generics so that Rust can do its job and comply with the Sized trait bound as much as possible.

Again thanks a lot for your time, that was really a huge help!

I think you're probably right about the interface implementation fiesta :slight_smile:

Keep at it, check out existing projects and/or other people's questions, and you'll build a good feel for the level of abstraction that works best in Rust.

1 Like