Vector of objects belonging to a trait
Asked Answered
P

3

90

Consider the following code:

trait Animal {
    fn make_sound(&self) -> String;
}

struct Cat;
impl Animal for Cat {
    fn make_sound(&self) -> String {
        "meow".to_string()
    }
}

struct Dog;
impl Animal for Dog {
    fn make_sound(&self) -> String {
        "woof".to_string()
    }
}

fn main () {
    let dog: Dog = Dog;
    let cat: Cat = Cat;
    let v: Vec<Animal> = Vec::new();
    v.push(cat);
    v.push(dog);
    for animal in v.iter() {
        println!("{}", animal.make_sound());
    }
}

The compiler tells me that v is a vector of Animal when I try to push cat (type mismatch)

So, how can I make a vector of objects belonging to a trait and calls the corresponding trait method on each element?

Preposition answered 12/9, 2014 at 23:2 Comment(0)
C
33

The existing answers explain the problem with Vec<Animal> well, but they use older syntax, which is not valid anymore.

In short, the vector needs to contain trait objects and its type should be (something like) Vec<Box<dyn Animal>>.

In modern Rust, the dyn keyword is used to specify a trait object. But we cannot use just Vec<dyn Animal>, because dyn Animal is not sized (Cat and Dog could pottentially have fields of different size). Vectors can only contain elements of a fixed size. So that's why in the vector we should rather store some sort of pointers to the actual structs. The Box struct is one such option, a kind of a smart pointer that has a fixed size in itself.

Let's test this (on a 64-bit machine):

use std::mem::size_of;
println!("size Cat = {}", size_of::<Cat>());  // 0 bytes (the Cat struct has no fields)
println!("size Dog = {}", size_of::<Dog>());  // 0 bytes (the Dog struct has no fields)
println!("size BoxCat = {}", size_of::<Box<Cat>>());  // 8 bytes (1 usize pntr)
println!("size BoxDyn = {}", size_of::<Box<dyn Animal>>());  // 16 bytes (2 usize pointers)
println!("{}", size_of::<dyn Animal>());  // Error: doesn't have a size known at compile-time

Note that if Cat had fields, size_of::<Cat>() would have been more than 0, but size_of::<Box<Cat>>() and size_of::<Box<dyn Animal>>() wouldn't change at all.

Also note that Box<dyn Animal> actually contains 2 pointers:

  • one that points to the actual struct instance data;
  • one for the vtable (that's because of dyn; it's needed for dynamic dispatching).

Now to your example. To make it work, you just need to replace these three lines:

let v: Vec<Animal> = Vec::new();
v.push(cat);
v.push(dog);    

with these:

let mut v: Vec<Box<dyn Animal>> = Vec::new();
v.push(Box::new(cat));
v.push(Box::new(dog));
Carter answered 1/1, 2023 at 11:32 Comment(1)
Very clear answer, this should be the accepted answer.Natty
S
101

Vec<Animal> is not legal, but the compiler can't tell you that because the type mismatch somehow hides it. If we remove the calls to push, the compiler gives us the following error:

<anon>:22:9: 22:40 error: instantiating a type parameter with an incompatible type `Animal`, which does not fulfill `Sized` [E0144]
<anon>:22     let mut v: Vec<Animal> = Vec::new();
                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The reason why that's not legal is that a Vec<T> stores many T objects consecutively in memory. However, Animal is a trait, and traits have no size (a Cat and a Dog are not guaranteed to have the same size).

To solve this problem, we need to store something that has a size in the Vec. The most straightforward solution is to wrap the values in a Box, i.e. Vec<Box<Animal>>. Box<T> has a fixed size (a "fat pointer" if T is a trait, a simple pointer otherwise).

Here's a working main:

fn main() {
    let dog: Dog = Dog;
    let cat: Cat = Cat;
    let mut v: Vec<Box<Animal>> = Vec::new();
    v.push(Box::new(cat));
    v.push(Box::new(dog));
    for animal in v.iter() {
        println!("{}", animal.make_sound());
    }
}
Sweat answered 13/9, 2014 at 2:6 Comment(5)
This seems to be using trait objects, i.e. dynamic dispatch. However, the Rust Programming Language book on this topic says "one can always have a thin statically-dispatched wrapper function that does a dynamic dispatch, but not vice versa, meaning static calls are more flexible." Is there actually a way to not use trait objects and only perform static dispatch in this case? I also encountered the use case where I want to have a vector storing potentially different types of structs implementing the same trait.Tramway
@JIXiang If you know in advance what concrete types you'll be putting in the Vec, you can use an enum instead (and then the trait might become redundant). Otherwise, no, you can't avoid dynamic dispatch. Keep in mind that dynamic dispatch is not inherently evil; there are situations where it's OK to use it!Anthropogenesis
The preferred way to write this is now Box<dyn Animal>.Kiefer
@Kiefer Now it's required.Urology
Yes, can confirm that the syntax is now expected as Box<dyn Animal>.Kristoforo
C
33

The existing answers explain the problem with Vec<Animal> well, but they use older syntax, which is not valid anymore.

In short, the vector needs to contain trait objects and its type should be (something like) Vec<Box<dyn Animal>>.

In modern Rust, the dyn keyword is used to specify a trait object. But we cannot use just Vec<dyn Animal>, because dyn Animal is not sized (Cat and Dog could pottentially have fields of different size). Vectors can only contain elements of a fixed size. So that's why in the vector we should rather store some sort of pointers to the actual structs. The Box struct is one such option, a kind of a smart pointer that has a fixed size in itself.

Let's test this (on a 64-bit machine):

use std::mem::size_of;
println!("size Cat = {}", size_of::<Cat>());  // 0 bytes (the Cat struct has no fields)
println!("size Dog = {}", size_of::<Dog>());  // 0 bytes (the Dog struct has no fields)
println!("size BoxCat = {}", size_of::<Box<Cat>>());  // 8 bytes (1 usize pntr)
println!("size BoxDyn = {}", size_of::<Box<dyn Animal>>());  // 16 bytes (2 usize pointers)
println!("{}", size_of::<dyn Animal>());  // Error: doesn't have a size known at compile-time

Note that if Cat had fields, size_of::<Cat>() would have been more than 0, but size_of::<Box<Cat>>() and size_of::<Box<dyn Animal>>() wouldn't change at all.

Also note that Box<dyn Animal> actually contains 2 pointers:

  • one that points to the actual struct instance data;
  • one for the vtable (that's because of dyn; it's needed for dynamic dispatching).

Now to your example. To make it work, you just need to replace these three lines:

let v: Vec<Animal> = Vec::new();
v.push(cat);
v.push(dog);    

with these:

let mut v: Vec<Box<dyn Animal>> = Vec::new();
v.push(Box::new(cat));
v.push(Box::new(dog));
Carter answered 1/1, 2023 at 11:32 Comment(1)
Very clear answer, this should be the accepted answer.Natty
W
25

You may use a reference trait object &Animal to borrow the elements and store these trait objects in a Vec. You can then enumerate it and use the trait's interface.

Altering the Vec's generic type by adding a & in front of the trait will work:

fn main() {
    let dog: Dog = Dog;
    let cat: Cat = Cat;
    let mut v: Vec<&Animal> = Vec::new();
    //             ~~~~~~~
    v.push(&dog);
    v.push(&cat);
    for animal in v.iter() {
        println!("{}", animal.make_sound());
    }
    // Ownership is still bound to the original variable.
    println!("{}", cat.make_sound());
}

This is great if you may want the original variable to keep ownership and reuse it later.

Keep in mind with the scenario above, you can't transfer ownership of dog or cat because the Vec has borrowed these concrete instances at the same scope.

Introducing a new scope can help handle that particular situation:

fn main() {
    let dog: Dog = Dog;
    let cat: Cat = Cat;
    {
        let mut v: Vec<&Animal> = Vec::new();
        v.push(&dog);
        v.push(&cat);
        for animal in v.iter() {
            println!("{}", animal.make_sound());
        }
    }
    let pete_dog: Dog = dog;
    println!("{}", pete_dog.make_sound());
}
Washout answered 15/1, 2017 at 21:9 Comment(1)
Thanks. This was the missing clue for me. In newer Rust version, I think there needs to be inserted a dyn, like this Vec<&dyn Animal>.Hevesy

© 2022 - 2024 — McMap. All rights reserved.