dyn , impl and Trait Objects — Rust

vikram fugro
3 min readJan 19, 2020

Let’s have a look at the following code:

trait Animal {
fn talk(&self);
}
struct Cat {}
struct Dog {}
impl Animal for Cat {
fn talk(&self) {
println!(“meow”);
}
}
impl Animal for Dog {
fn talk(&self) {
println!(“bark”);
}
}
fn animal_talk(a: &dyn Animal) {
a.talk();
}
fn main() {
let d = Dog {};
let c = Cat {};
animal_talk(&d);
animal_talk(&c);
}

We see that animal_talk()is invoked for both Catand Dog. But the function is not aware of the exact types. dyn tells the compiler to not determine the exact type and just be content with a reference to some type implementing the trait Animal. This is called dynamic dispatch and the type is determined at the runtime, so there is a runtime overhead.

But why is there a & just before dyn ? “Pointer to a trait ?” Yes! Since the type (and hence the size of the concrete type) is not known at compile time; it is illegal to just have something like this:

fn animal_talk(a: dyn Animal) {
a.talk();
}

The compiler will complain saying the size is not known.

Rust calls this a trait object (& dyn Animal). It represents a pointer to the concrete type and a pointer to a vtable of function pointers. Box<dyn Animal>, Rc<dyn Animal> are also trait Objects. They too contain a pointer to a concrete type allocated on the heap, that satisfies the given trait.

Now, let’s have a look a the following code:

fn animal_talk(a:impl Animal) {
a.talk();
}
fn main() {
let c = Cat{};
let d = Dog{};

animal_talk(c);
animal_talk(d);
}

This compiles fine, and there is no & there as well. impl here makes the compiler determine the type at the compile time, which means the compiler will do some name mangling and will have two variants of the functions; One that takes Dog and another that takes Cat. This is called monomorphization and will not have any runtime overhead, but will lead to code bloat.

Consider the following code; Will this compile or not?

fn animal () -> impl Animal {
if (is_dog_available()) {
return Dog {};
}
Cat {}
}

It fails! because, the types here are determined at the compile time (static dispatch) . The error is pretty self-explanatory:

error[E0308]: mismatched types
|
| fn animal() -> impl Animal {
| ----------- expected because this return type...
| if (is_dog_available()) {
| return Dog {};
| ------ ...is found to be `Dog` here
| }
|
...
| Cat {}
| ^^^^^^ expected struct `Dog`, found struct `Cat`
|
= note: expected type `Dog`
found type `Cat`

The compiler here first zeroes down to Dog as the return type, and will expect the same type for rest of the return points within the function body.

Now, what about the following code?

fn animal() -> Box<dyn Animal> {
if (is_dog_available()) {
return Box::new(Dog {});
}

Box::new(Cat {})
}

Compiler would be happy with this, as it doesn’t have to know the exact return type and will happily compile this code (dynamic dispatch). All it needs is some boxed type (Trait Object) satisfying the given trait.

Object Safety

A trait is not considered object safe if in it’s methods’, the type that implements it, features as a value (and not as a reference).

Consider the following code:

trait Animal {
fn nop(self) {}
}
struct Cat {}impl Animal for Cat {}fn main() {
let c = Cat {};
let ct:&dyn Animal = &c;
c.nop();
}

It gives the error:

error[E0277]: the size for values of type `Self` cannot be known at compilation time
|
| fn nop(self) {}
| ^^^^ - help: consider further restricting `Self`: `where Self: std::marker::Sized`
| |
| doesn't have a size known at compile-time
|
= help: the trait `std::marker::Sized` is not implemented for `Self`

nop() receives the type by value. But since this is dynamic dispatch, the type and hence it’s size is not known. Even if we put the Sized bound on Self, i.e fn nop(self) where Self:Sized {} the compiler would still complain, as there’s no way to know the size at the compilation time and also it will introduce dependency on Self (i.e the type as a value) in the code.

That’s it ! Hope this was helpful in understanding the motive behind having impl and dyn and how object safety for a trait is critical when using dynamic dispatch.

--

--

vikram fugro

Open Source Software Enthusiast, Polyglot & Systems Generalist.