Fat Pointers
A fat pointer (also called a wide pointer) is a two-word pointer (2 * size_of::<usize>() bytes—16 bytes on 64-bit targets, 8 bytes on 32-bit targets) that pairs a memory address with a second word of runtime metadata required to access a dynamically sized type (DST).
Thin Pointers vs. Fat Pointers
A pointer to a Sized type (&u64, &Vec<u8>, Box<[u8; 4]>) is a thin pointer: a single usize word holding only the target memory address, because the pointee's size and method implementations are known at compile time.
When a type is unsized ([T], str, dyn Trait, or a struct whose final field is unsized), any reference or smart pointer to that type (&T, &mut T, Box<T>, Rc<T>, Arc<T>, *const T, *mut T) automatically widens to carry two words:
Thin Pointer: &[u32; 3] (8 bytes on 64-bit)
+------------------+
| data_ptr (8 B) --+------> [ 10 | 20 | 30 ]
+------------------+
Slice Fat Pointer: &[u32] (16 bytes on 64-bit)
+------------------+------------------+
| data_ptr (8 B) | len = 3 (8 B) |
+--------+---------+------------------+
|
+----------------> [ 10 | 20 | 30 ]
Trait-Object Fat Pointer: &dyn Draw (16 bytes on 64-bit)
+------------------+------------------+
| data_ptr (8 B) | vtable_ptr (8 B) |
+--------+---------+--------+---------+
| |
v v
Concrete Value +--------------------+
| drop_in_place fn |
| size (usize) |
| align (usize) |
| Draw::draw fn ptr |
+--------------------+
Comparing Thin and Fat Pointer Sizes
| Pointer Type | Pointee Kind | Size (64-bit) | First Word | Second Word |
|---|---|---|---|---|
&u32, Box<u32>, *const u32 |
Sized |
8 bytes |
Data address | None |
&Vec<u8>, &String |
Sized container |
8 bytes |
Address of stack header | None |
&[u32], Box<[u32]>, *const [u32] |
Slice ([T]) |
16 bytes |
Address of first element | Element count (usize) |
&str, Box<str>, Arc<str> |
String slice (str) |
16 bytes |
Address of first UTF-8 byte | Byte length (usize) |
&dyn Trait, Box<dyn Trait> |
Trait object | 16 bytes |
Address of concrete instance | Static vtable address |
trait Summary {
fn summarize(&self) -> usize;
}
impl Summary for u64 {
fn summarize(&self) -> usize {
*self as usize
}
}
fn main() {
println!("&u64: {} bytes", size_of::<&u64>());
println!("&[u64; 4]: {} bytes", size_of::<&[u64; 4]>());
println!("&[u64]: {} bytes", size_of::<&[u64]>());
println!("Box<[u64]>: {} bytes", size_of::<Box<[u64]>>());
println!("&str: {} bytes", size_of::<&str>());
println!("&dyn Summary: {} bytes", size_of::<&dyn Summary>());
}
&u64: 8 bytes
&[u64; 4]: 8 bytes
&[u64]: 16 bytes
Box<[u64]>: 16 bytes
&str: 16 bytes
&dyn Summary: 16 bytes
Converting Between [T; N], Vec<T>, and Box<[T]>
A Vec<T> stores three words on the stack (ptr, len, cap = 24 bytes on 64-bit). Once a vector's length no longer needs to grow, calling Vec::into_boxed_slice() drops any excess capacity and shrinks the stack handle to a two-word Box<[T]> fat pointer (16 bytes).
fn main() {
let mut vec = Vec::with_capacity(10);
vec.extend_from_slice(&[1_u32, 2, 3]);
println!("Vec<u32> stack size: {} bytes (cap = {})", size_of_val(&vec), vec.capacity());
let boxed_slice: Box<[u32]> = vec.into_boxed_slice();
println!("Box<[u32]> stack size: {} bytes (len = {})", size_of_val(&boxed_slice), boxed_slice.len());
}
Vec<u32> stack size: 24 bytes (cap = 10)
Box<[u32]> stack size: 16 bytes (len = 3)