use std::mem::size_of; struct Dog; struct Cat; trait Speak { fn speak(&self); } impl Speak for Dog { fn speak(&self) { println!("Bow bow"); } } impl Speak for Cat { fn speak(&self) { println!("Meow meow"); } } fn speak_static(animal: &impl Speak) { animal.speak(); } fn speak_dynamic(animal: &dyn Speak) { animal.speak(); } fn main() { let c = Cat; let d = Dog; speak_static(&c); speak_dynamic(&d); println!("Size of &Dog: {} bytes", size_of::<&Dog>()); println!("Size of &Cat: {} bytes", size_of::<&Cat>()); println!("Size of &dyn Speak: {} bytes", size_of::<&dyn Speak>()); println!( "Size of Box<dyn Speak>: {} bytes", size_of::<Box<dyn Speak>>() ); let boxed_dog: Box<dyn Speak> = Box::new(Dog); let dyn_ref: &dyn Speak = &d; println!( "Address of boxed_dog (fat pointer on stack): {:p}", &boxed_dog ); println!("Address of d: {:p}", &d as *const Dog as *const ()); println!("Address of dyn_ref (fat pointer on stack): {:p}", &dyn_ref); // Use raw pointers to get the data pointer from the fat pointer let data_ptr = unsafe { // Extract just the data pointer from the fat pointer let fat_ptr_addr: *const *const () = &dyn_ref as *const _ as *const *const (); *fat_ptr_addr }; println!("Data pointer inside dyn_ref fat pointer: {:p}", data_ptr); }
Yes — your code is correct, and you're exploring very important low-level Rust concepts beautifully:
- Trait objects
- Fat pointers
- Raw pointer manipulation
I'll walk through and confirm your experiment:
✅ Static and Dynamic Dispatch
#![allow(unused)] fn main() { fn speak_static(animal: &impl Speak) // static dispatch fn speak_dynamic(animal: &dyn Speak) // dynamic dispatch }
&impl Speakgets monomorphized at compile-time → fast, no vtable lookup.&dyn Speakis a fat pointer (data pointer + vtable pointer) → dynamic dispatch.
You correctly showed speak_static(&c) and speak_dynamic(&d) — perfect.
✅ Memory Layout & Sizes
#![allow(unused)] fn main() { println!("Size of &Dog: {} bytes", size_of::<&Dog>()); println!("Size of &Cat: {} bytes", size_of::<&Cat>()); println!("Size of &dyn Speak: {} bytes", size_of::<&dyn Speak>()); println!("Size of Box<dyn Speak>: {} bytes", size_of::<Box<dyn Speak>>()); }
The sizes make total sense:
&Dogand&Catare thin pointers → 8 bytes (just a memory address on x86_64).&dyn Speakis a fat pointer → 16 bytes (8 bytes for data pointer + 8 bytes for vtable pointer).Box<dyn Speak>also stores a fat pointer on the stack → 16 bytes.
💬 Remember: Box<dyn T> itself (on stack) stores the fat pointer; the actual heap allocation is just the object data (Dog), the vtable is elsewhere.
✅ Inspecting Raw Pointers
#![allow(unused)] fn main() { let data_ptr = unsafe { let fat_ptr_addr: *const *const () = &dyn_ref as *const _ as *const *const (); *fat_ptr_addr }; }
You're correctly:
- Treating
&dyn Speakas a*const *const (), i.e., a pointer to a data pointer. - Dereferencing to get the data pointer alone (first 8 bytes of fat pointer).
- Printing the extracted pointer (
data_ptr), which matches the address ofd(correct!).
🔥 Summary of what your experiment shows:
| Concept | Result |
|---|---|
&T size | 8 bytes (thin pointer) |
&dyn Trait size | 16 bytes (fat pointer) |
Box<dyn Trait> size | 16 bytes (fat pointer on stack) |
| How fat pointers store data | Data pointer + vtable pointer |
| Accessing inner data pointer manually | Correctly done |
🔥 Bonus: Small Tip for an Even Clearer Version
You might make the raw pointer extraction a tiny bit clearer like this (using std::ptr::metadata and std::ptr::from_raw_parts):
#![allow(unused)] fn main() { let data_ptr = { let (data, _vtable): (*const (), *const ()) = unsafe { let (data, vtable): (*const (), *const ()) = std::mem::transmute(dyn_ref); (data, vtable) }; data }; }
But your way is perfectly valid and educational for understanding the raw memory layout.
🏆 Final Evaluation
✅ Static and dynamic dispatch used correctly
✅ Sizes match expected memory layouts
✅ Address analysis is correct
✅ Raw pointer extraction is correct
✅ Excellent for understanding Rust trait objects internally
Would you like me to show you a diagram that visually explains how a &dyn Speak pointer looks in memory (stack + heap)? 📈 It might make this even more intuitive! 🚀