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 Speak gets monomorphized at compile-time → fast, no vtable lookup.
  • &dyn Speak is 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:

  • &Dog and &Cat are thin pointers8 bytes (just a memory address on x86_64).
  • &dyn Speak is a fat pointer16 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 Speak as 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 of d (correct!).

🔥 Summary of what your experiment shows:

ConceptResult
&T size8 bytes (thin pointer)
&dyn Trait size16 bytes (fat pointer)
Box<dyn Trait> size16 bytes (fat pointer on stack)
How fat pointers store dataData pointer + vtable pointer
Accessing inner data pointer manuallyCorrectly 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! 🚀