In this lesson, you will learn how Rust achieves dynamic dispatch by decomposing a trait object into a dual-pointer structure known as a wide pointer.
When you use a type like Box<dyn Trait>, you aren't just holding a reference to data on the heap. You are holding a fat pointer (also called a wide pointer) that is twice the size of a standard pointer. This structure consists of two distinct components: the data pointer and the vtable pointer.
The Anatomy of the Fat Pointer
- The Data Pointer: This points to the actual instance of the concrete type (e.g., a
struct User) stored on the heap. Rust needs this to pass theselfargument to methods. - The Vtable Pointer: This points to a "virtual method table" (vtable). The vtable is a static structure generated by the compiler for every unique combination of a type and a trait. It contains the memory addresses of the function pointers for each method defined in the trait.
Visualizing the Structure
Consider a trait Speak and a struct Dog that implements it. When you perform let x: Box<dyn Speak> = Box::new(Dog);, the memory layout looks like this:
[ Data Pointer ] -> [ Dog Instance (Heap) ]
[ Vtable Pointer ] -> [ Vtable (Static Memory) ]
- Pointer to <Dog as Speak>::speak
- Pointer to drop glue (cleanup)
- Size/Alignment metadata
When you call x.speak(), the compiler does not know what type x holds at compile time. Instead, it performs a two-step lookup:
- It follows the vtable pointer to find the correct
speakfunction implementation for the concreteDogtype. - It calls that function, passing the data pointer as the
selfargument.
Because this lookup happens at runtime, we trade the static "inlining" optimizations we discussed in previous lessons for the flexibility of type erasure. While this indirection incurs a small performance cost—specifically the pointer chasing and the inability to inline the target function—it is the foundational mechanism that allows us to store heterogeneous collections of types that share the same behavior.
Key Takeaway: A trait object is a wide pointer composed of a data pointer to the instance and a vtable pointer to the function implementations, enabling runtime polymorphism via indirection.