How to make a struct that holds a vector/collection of trait objects?


#1

Here’s a contrived example of what i’m trying to achieve.

trait AwesomeTrait {
    fn awesomeness (&self) {
        println!("Awesome Trait!");
    }
}

struct AwesomeStruct;

impl AwesomeTrait for AwesomeStruct {}

struct AwesomeStruct2;

impl AwesomeTrait for AwesomeStruct2 {}

struct ListOfAwesomeStruct<S>
where S: AwesomeTrait {
    list: Vec<S>
}

impl<S: AwesomeTrait> ListOfAwesomeStruct<S> {
    fn new () -> Self {
        Self {
            list: Vec::new(),
        }
    }
    
    fn push(&mut self, awesome: S) -> &mut Self {
        self.list.push(awesome);
        
        self
    }
}

fn main() {
    let ooo = ListOfAwesomeStruct::new();
    ooo.push(AwesomeStruct);
    ooo.push(AwesomeStruct2); /// cannot execute this push because `ooo` is now of type `ListOfAwesomeStruct<AwesomeStruct>`
}

errors

error[E0308]: mismatched types
  --> src/main.rs:37:14
   |
37 |     ooo.push(AwesomeStruct2);
   |              ^^^^^^^^^^^^^^ expected struct `AwesomeStruct`, found struct `AwesomeStruct2`
   |
   = note: expected type `AwesomeStruct`
              found type `AwesomeStruct2`

error: aborting due to previous error

error: Could not compile `playground`.

To learn more, run the command again with --verbose.

play.rust-lang link.

any ideas as to how i can get this working?


#2

https://play.rust-lang.org/?gist=b5a3be0dc0127e22360726542a66b193&version=stable

You need to store trait objects (i.e. erased AwesomeTrait values). You can also make caller give you a Box<AwesomeTrait> instead of having the generic function there - I left the generic version though.


#3

thanks a lot!


#4

So i just tried it out for my actual use case, but it appears my trait cannot be made into an object :disappointed:


#5

My trait actually requires other trait bounds to be satisfied


use std::collections::HashMap;
trait AwesomeTrait: Clone + Sized {
    fn awesomeness (&self) {
        println!("Awesome Trait!");
    }
}

struct AwesomeStruct;

impl AwesomeTrait for AwesomeStruct {}

struct AwesomeStruct2;

impl AwesomeTrait for AwesomeStruct2 {}

struct ListOfAwesomeStruct{
    list: HashMap<String, Box<AwesomeTrait>>
}

impl ListOfAwesomeStruct {
    fn new () -> Self {
        Self {
            list: HashMap::new(),
        }
    }
    
    fn push<S: AwesomeTrait + 'static>(&mut self, key: String,awesome: S) -> &mut Self {
        self.list.insert(key, Box::new(awesome));
        
        self
    }
}

fn main() {
    let mut ooo = ListOfAwesomeStruct::new();
    ooo.push("key1".to_owned(), AwesomeStruct);
    ooo.push("key2".to_owned(), AwesomeStruct2);
}

play ground


#6

Why did you make AwesomeTrait depend on Clone + Sized?


#7

Clone so i can clone the trait object. Sized so i didn’t have to deal with some weird compiler complaints
here’s the actual trait

pub trait Service: Clone + Sized {
	fn call (&self, req: hyper::Request) -> Box<Future<Item = hyper::Response, Error = hyper::Error>>;
}


#8

Signature of clone is fn clone(&self) -> Self, that is compiler must know the size of type which implements Clone to be able to allocate stack space for the returned value. Size of a trait object is unknown at compile time, thus Clone can’t be implemented for trait objects.

You can mitigate this by adding your own version of the clone function like this.


#9
#[derive(Clone)]
struct ListOfAwesomeStruct{
    list: HashMap<String, Box<AwesomeTrait>>
}

list of awesome struct also needs to be cloneable, that’s why i wanted the trait objects themselves to be cloneable, else you get a weird error.

playground


#10

Here is one possible way to resolve this:

impl Clone for Box<AwesomeTrait> {
    fn clone(&self) -> Self {
        self.boxed_clone()
    }
}

This works, unlike implementing Clone for AwesomeTrait directly, because you are manipulating Boxes all the time and those have a well-defined size. The compiler can thus stack-allocate the space for the cloned box.


#11

@seunlanlege, what’s the reason you need Clone here? Do you actually need/want to clone the trait object or are you trying to bypass lifetime issues? If it’s the latter, you can put your trait objects inside Rc (or Arc if you need thread-safety) and then have a refcounted pointer to the single instance of each of them. Note that if you did go with Rc/Arc and needed to borrow the trait object mutably (i.e. &mut self), you’d need to wrap the trait object in a RefCell in addition (i.e. Rc<RefCell<AwesomeTrait>>).


#12

I just tried this out and you’re a genius!!


#13

I need multiple owners of a struct that holds trait objects in a multithread context, (that was a mouthful). I initially tried using Arc, but Arc moves the value. so no Arc didn’t work for me.


#14

Can you expand on the “Arc moves the value” and why that didn’t work? The Box also moves the value.


#15

when spawning threads e.g

impl ArbitraryTrait for ListOfAwesomeStruct {}

fn contrived<S>(structOfTraitObjects: S)
 where 
     S: ArbitraryTrait + Send + Sync + 'static
{
   let structOfTraitObjects = Arc::new(structOfTraitObjects);
   for _ in 1..4 {
      thread::spawn(move || {
        funcThatExpectsArbitraryTraitObject(structOfTraitObjects.clone());
       //  can't do this, the first iteration of thread::spawn moves structOfTraitObjects
     }
   }
}

where i could just do

impl ArbitraryTrait for ListOfAwesomeStruct {}

fn contrived<S>(structOfTraitObjects: S)
 where 
     S: ArbitraryTrait + Send + Sync + Clone + 'static
{
   let structOfTraitObjects = structOfTraitObjects.clone();
   for _ in 1..4 {
      thread::spawn(move || {
        funcThatExpectsArbitraryTraitObject(structOfTraitObjects.clone());
       // Awwww, yeaaahhhh!
     }
   }
}

#16

although if you have a better way, please recommend!


#17

It is the closure which moves structOfTraitObjects, not Arc.

This will work

let struct_of_trait_objects = Arc::new(struct_of_trait_objects);
for _ in 1..4 {
    let cloned_arc = struct_of_trait_objects.clone();
    thread::spawn(move || {
        funcThatExpectsArbitraryTraitObject(cloned_arc);
        ...

#18

what @red75prime said :slight_smile: You need to move a clone of the Arc, not the original one.


#19

yeah, that’s a much better approach.


#20

Thanks for the help guys, really learnt a lot today :bowing_man: