Search code examples
rusttraitstrait-objects

What happens when I pass a concrete struct reference to a function that takes trait objects?


In the following code I have a simple trait, A, and a struct Foo that implements A...

Next, I define a function that takes a reference to a trait object. From main() I pass in a reference to a concrete Foo. This works fine and successfully calls the method in the trait.

trait A {
    fn do_something(&self) {
        println!("Doing something.");
    }
}

struct Foo {}
impl A for Foo {}

fn main() {
    let foo = Foo {};
    make_do_something(&foo);
}

fn make_do_something(a: &dyn A) {
    a.do_something();
}

What is the Rust compiler doing here? Is the concrete Foo reference automatically coerced to a Trait object reference? That is, is a new trait object being created on the heap that is referring to my concrete object? I can't find a clear description of how this works in the documentation.


Solution

  • Is the concrete Foo reference automatically coerced to a Trait object reference?

    Yes, the &Foo is coerced to &dyn A. The &Foo consists of a pointer to the data of the Foo (which happens to be zero bytes long in your example, but this makes no difference); the &dyn A consists of that pointer and also a pointer to the vtable generated from impl A for Foo.

    is a new trait object being created on the heap

    No; a trait object demands additional data to refer to it, not to store it.

    Compare this with the other main kind of dynamically-sized (!Sized) type in Rust, the slice: a &Foo contains a pointer to a Foo, and a &[Foo] contains that pointer and a length. They might be the same data pointed to (you can convert both ways), but the pointer is different.

    We can say in general that pointing to a value of a Sized type requires only the machine pointer to the data; pointing to a value of a !Sized type requires additional information (called the “metadata”).

    Every pointer to a dynamically-sized type is constructed either by a coercion, in which case the compiler inserts the necessary data that is known at compile time (vtable for a trait object, or length for a slice pointer coerced from an pointer array), or by library code which knows something about what it's constructing a pointer for (e.g. when Vec<T> produces a &[T]).