Fat Pointers and Vtables
A pointer or reference to a trait object (&dyn Trait, Box<dyn Trait>, Arc<dyn Trait>) is represented at runtime as a two-word fat pointer (wide pointer): one machine word pointing to the concrete value's data, and a second machine word pointing to a static virtual method table (vtable).
Memory Layout of a Trait Object Fat Pointer
On a 64-bit architecture, a regular reference &Concrete is 8 bytes, whereas a trait object reference &dyn Draw is 16 bytes (2 * usize):
Stack (`&dyn Draw`, 16 bytes) Heap / Stack Data
+------------------+ +-----------------------+
| data_ptr (8 B) -+---------------> | Circle { radius: 5.0 }|
+------------------+ +-----------------------+
| vtable_ptr (8 B)-+---+
+------------------+ | Static Read-Only Memory (vtable for Circle as Draw)
| +----------------------------------------+
+-----------> | drop_in_place: fn(*mut Circle) |
| size: 8 bytes |
| align: 8 bytes |
| Draw::draw: <Circle as Draw>::draw |
+----------------------------------------+
What the Compiler Stores Inside a Vtable
For each (ConcreteType, Trait) pair used as a trait object in the program, the compiler emits one immutable vtable in static read-only memory (.rodata) containing:
drop_in_placepointer: A function pointer to the concrete type's destructor soBox<dyn Trait>can properly drop the underlying value when freed.sizeandalign: The exact byte size (usize) and alignment (usize) of the concrete type soBox<dyn Trait>knows how many bytes to deallocate on drop.- Method pointers: One function pointer for each
dyn-compatible method in the trait (and its supertraits).
Verifying Pointer Sizes with std::mem::size_of
use std::mem::size_of;
trait Draw {
fn draw(&self);
}
struct Button {
width: u32,
height: u32,
}
impl Draw for Button {
fn draw(&self) {}
}
fn main() {
println!("size_of::<Button>(): {} bytes", size_of::<Button>());
println!("size_of::<&Button>(): {} bytes (thin pointer)", size_of::<&Button>());
println!("size_of::<&dyn Draw>(): {} bytes (fat pointer)", size_of::<&dyn Draw>());
println!("size_of::<Box<dyn Draw>>(): {} bytes (fat pointer)", size_of::<Box<dyn Draw>>());
}
size_of::<Button>(): 8 bytes
size_of::<&Button>(): 8 bytes (thin pointer)
size_of::<&dyn Draw>(): 16 bytes (fat pointer)
size_of::<Box<dyn Draw>>(): 16 bytes (fat pointer)
Even for a zero-sized unit struct (struct Noop;), &Noop is 8 bytes (data_ptr is a non-null aligned dangling address and size in the vtable is 0), while &dyn Draw for Noop is still 16 bytes because the vtable pointer is stored inline inside the fat pointer rather than inside the struct itself.