Simple polymorphism: making a Farm with different Animals


#1

I have an Animal trait containing a make_noise function, and a Dog and Cow both implementing that trait.

I now want to make a Farm that “owns” these two animals (Vec<Animal>). The farm has a good_morning method that calls make_noise for all owned animals:

https://play.rust-lang.org/?gist=aad486538a707ae32fbe31c618a4b738&version=stable&backtrace=0

I get several errors:

  • the trait bound Animal + 'static: std::marker::Sized is not satisfied
  • the trait Animal cannot be made into an object

I tried using Vec<&Animal> and adding lifetime specifiers, but I still get the same error:

https://play.rust-lang.org/?gist=a0197ec40726483d01a4420638b06f86&version=stable&backtrace=0

I’m not sure how to implement the Sized trait, and if that would be relevant here.


#2

You probably want the farm to own the animals. The way to do that is to store trait objects - use a Vec<Box<Animal>> for storage.

You also need to make Animal object safe, and currently it’s not because the fn defined there has no receiver - make it take &self.

https://play.rust-lang.org/?gist=d3c5c903b767b88208979ea8e03d5c11&version=stable&backtrace=0


#3

Thanks to the reference to trait objects. I found the description + examples of them in the second edition of the nightly book very useful:

https://doc.rust-lang.org/nightly/book/second-edition/ch17-02-trait-objects.html

In your example, is there a way in main to clean up the API and just use farm.add_animal(Dog {}) (without the Box::new) and doing the “boxing” in add_animal? I’m imagining that would require copying the Dog object?


#4

Yes. You can do this:

fn add_animal<T: Animal + 'static>(&mut self, animal: T) {
        self.animals.push(Box::new(animal));
    }

Note that I wouldn’t do that because it hides a memory allocation behind an otherwise generic function. You also need to add the 'static lifetime bound if you don’t want Farm to carry a lifetime parameter itself.


#5

Wow OK that’s a lot of new syntax. :astonished:

Any good references that could help me unpack that? I think the <T: Animal + 'static> is a lifetime specifier?


#6

It’s a trait lifetime bound. The topic to learn would be “trait object lifetime bounds”, and the default trait object lifetime, which happens to be 'static.

What the above leads to is Box<Trait> is actually Box<Trait + 'static> because the default trait lifetime bound is 'static. This says the trait object is tied to the static lifetime, which essentially means the trait object (i.e. the real type implementing the trait) either has no references or only 'static references. Since Rust is keen on ensuring memory safety, and traits can be implemented for types that carry references or are references themselves, there are times when you need to explicitly indicate the lifetime bounds on the trait objects.

The above hopefully explains why you need to tell Rust that the Animal generic type also has the 'static lifetime (i.e. because you’re going to put into a Box, which requires that) in order to be a valid type argument.


#7

I just want to take a minute to note that you can also do all of this by instead having an Animal enum for which you can add a good_morning method. This method would match it’s variants, figure out which animal it is, and call it’s make_noise. This will reduce the amount of new syntax and it is, in some sense semantically the same as what you want. The big difference is that you can’t do this when you don’t know the animals before hand (as for performance if you are curious, I’d image it would be nearly identical)


#8

The trait object approach is a full on dynamic dispatch - there’s basically zero chance the compiler will inline the call. With the enum, the target is statically known and inlining has a chance to occur. Whether that makes a big difference would depend on the situation :slight_smile:.

It’s good that you mentioned this approach too though, for completeness sake. There are tradeoffs with each such that some situations heavily call for one and not the other, but it’s good to have choices :grinning:


#9

Not necessarily true. While Rust/LLVM doesn’t perform this optimization, gcc can actually do so. For instance, in this C++ code…

class Example {
public:
    virtual int method();
};

int Example::method() {
    return 1;
}

int get_that_value(Example* ex) {
    return ex->method();
}

This is compiled to the following assembly code.

Example::method():
        mov     eax, 1
        ret
get_that_value(Example*):
        mov     rax, QWORD PTR [rdi]
        mov     rax, QWORD PTR [rax]
        cmp     rax, OFFSET FLAT:Example::method()
        jne     .L5
        mov     eax, 1
        ret
.L5:
        jmp     rax

Note that the assembly code first checks if a method is Example::method (whose code is known), if it is, it is called (well, inlined in this case) without dynamic dispatch, letting branch prediction actually work.


#10

That’s why I qualified my statement with “basically” :slight_smile:. GCC has some support for devirtualization, and perhaps LLVM (or rustc or another backend) may get it at some future point as well. However, even if present, it’ll likely be quite limited in effectiveness. For instance, try to compile that example but with a lot more subclasses of Example defined. Without PGO, I wouldn’t expect GCC to sprinkle type checks and inline code liberally across many concrete types - that would clog up branch prediction and code cache.

This optimization is very prevalent in JVMs like Hotspot, but there they have PGO built-in and know the universe of types loaded at any given time. But even there devirtualization is limited to just a couple of different receiver types - beyond that it goes into virtual dispatch as well (with some caveats like still having a special branch for a type that’s seen very frequently).

My take on things like this is if inlining is important, then write the code that makes inlining as likely to happen as possible, i.e. help the compiler.


#11

In my example I was looking for fully dynamic dispatch; so being able to build an Animal class that could be implemented by my code and potentially other libraries. I assume Rust uses something like vtables in C++ in this case; I’m fine with some run-time overhead for this flexibility.