struct Dog; struct Cat;

trait Speak { fn speak(&self); }

impl Speak for Dog { fn speak(&self) { println!("Woof woof!"); } }

impl Speak for Cat { fn speak(&self) { println!("Meow meow"); } }

fn speak_static<T: Speak>(animal: T) { animal.speak(); } fn speak_dynamic(animal: &dyn Speak) { animal.speak(); } fn main() { let d = Dog; speak_static(d);

let c = Cat;
let a = &c;

speak_dynamic(a);

}

The vtable for each concrete type-trait implementation pair is created during compilation and stored as a static data structure in the final binary. Let me explain in more detail:

When the Rust compiler processes your code, it analyzes all implementations of traits for various types. For each implementation (like impl Speak for Cat), it generates a vtable - a table of function pointers and metadata.

This table contains:

  1. A pointer to a drop_in_place function (for proper cleanup)
  2. Size information about the type
  3. Alignment requirements
  4. Function pointers to each method implementation of the trait

For your example, the compiler creates:

  • A vtable for Cat implementing Speak
  • A vtable for Dog implementing Speak

These vtables are embedded in your program's binary at fixed memory locations. They're similar to read-only static data structures.

When code execution reaches the point where a trait object is created (the coercion point), the compiler has inserted instructions that:

  1. Take the address of your concrete type instance (&c in your example)
  2. Take the address of the appropriate pre-compiled vtable (based on the concrete type and trait)
  3. Combine these two addresses to form the fat pointer

The compiler knows which vtable to use because it has full type information at the coercion point. It's essentially generating code like this (pseudo-code):

#![allow(unused)]
fn main() {
// Instead of this simple coercion:
speak_dynamic(a);  // a: &Cat coerced to &dyn Speak

// The compiler generates something conceptually like:
let trait_object = TraitObject {
    data: a,
    vtable: &VTABLE_FOR_CAT_AS_SPEAK  // Address of the static vtable
};
speak_dynamic(trait_object);
}

VTABLE_FOR_CAT_AS_SPEAK is a static data structure at a fixed memory address in your program's binary.

When you call a method on the trait object, the program uses the vtable pointer to find the right implementation based on the concrete type, without needing to know what that type is at compile time.


Yes, you're absolutely right. The compiler could determine the vtable more generically based on the reference a rather than needing to explicitly know it's a Cat.

Conceptually, it would be more like:

#![allow(unused)]
fn main() {
// Instead of this simple coercion:
speak_dynamic(a);  // a: &Cat coerced to &dyn Speak

// The compiler could generate something like:
let vtable_for_a = get_vtable_for_type::<typeof(*a), Speak>();
let trait_object = TraitObject {
    data: a,
    vtable: vtable_for_a  // Address of the appropriate vtable for a's type
};
speak_dynamic(trait_object);
}

Where get_vtable_for_type is a compile-time function that determines the correct vtable address based on the concrete type being coerced and the trait being implemented.

This is more accurate because it reflects how the compiler can handle trait objects generically without hardcoding type names. The compiler just needs to know:

  1. What concrete type is being coerced
  2. What trait interface is being requested

From these two pieces of information, it can locate the appropriate vtable. This is why trait objects work seamlessly with generics and in contexts where the concrete type isn't explicitly named.