Dynamically Sized Types
A Dynamically Sized Type (DST) is an unsized type whose runtime size or behavior metadata is stored inline inside the second word of a 2-word fat pointer (data_ptr + metadata) whenever a reference or smart pointer to the DST is created.
Fat Pointer Metadata for Slice DSTs vs. Trait Object DSTs
On a 64-bit target, every pointer to a Sized type (&i32, Box<String>) is a single 8-byte memory address (thin pointer). Every pointer to a DST (&[T], &str, &dyn Trait, Box<[T]>, Arc<dyn Trait>) is a 16-byte fat pointer whose second 8-byte word stores the runtime metadata needed to use and drop the value:
1. Slice DST Fat Pointer (`&[T]`, `&str`, `Box<[T]>`, `Box<str>`):
+-----------------------+-----------------------+
| data_ptr: *const T | len: usize (count) | (16 bytes on 64-bit)
+-----------------------+-----------------------+
2. Trait Object DST Fat Pointer (`&dyn Trait`, `Box<dyn Trait>`):
+-----------------------+-----------------------+
| data_ptr: *const () | vtable_ptr: *const () | (16 bytes on 64-bit)
+-----------------------+-----------------------+
| DST Pointer Type | Word 1 (8 B) |
Word 2 (8 B Metadata) |
How size_of_val(&dst) Is Computed at Runtime |
|---|---|---|---|
&[T] / Box<[T]> |
Address of element 0 |
Element count len: usize |
len * size_of::<T>() |
&str / Box<str> |
Address of byte 0 |
Byte length len: usize |
len bytes |
&dyn Trait / Box<dyn Trait> |
Address of concrete struct | Pointer to static vtable | Read from the size field inside the vtable |
Querying Runtime Size and Alignment of a DST with size_of_val and align_of_val
While std::mem::size_of::<T>() and align_of::<T>() require T: Sized at compile time, std::mem::size_of_val(val: &T) and std::mem::align_of_val(val: &T) accept T: ?Sized and compute the exact byte size and alignment of any DST at runtime from its fat-pointer metadata:
use std::fmt::Display;
use std::mem::{align_of_val, size_of_val};
fn inspect_dst<T: ?Sized>(label: &str, value: &T) {
println!(
"{label:<18} -> runtime size = {:>2} B, align = {} B (ref size = {} B)",
size_of_val(value),
align_of_val(value),
size_of_val(&value)
);
}
fn main() {
let slice: &[u32] = &[10, 20, 30];
let text: &str = "hello";
let trait_obj: &dyn Display = &42_u64;
inspect_dst("&[u32] (3 items)", slice);
inspect_dst("&str (5 bytes)", text);
inspect_dst("&dyn Display (u64)", trait_obj);
}
&[u32] (3 items) -> runtime size = 12 B, align = 4 B (ref size = 16 B)
&str (5 bytes) -> runtime size = 5 B, align = 1 B (ref size = 16 B)
&dyn Display (u64) -> runtime size = 8 B, align = 8 B (ref size = 16 B)
Custom Structs With an Unsized Tail Field (Custom DSTs)
A struct's final field (and only its final field) is allowed to be a DST ([T], str, or dyn Trait), making the struct itself a custom DST. Standard library types such as std::path::Path (struct Path { inner: OsStr }) and Rc<T> / Arc<T> internal allocation boxes use this layout to store a fixed header (strong_count, weak_count) and a variable-length [T] or str payload in a single heap allocation:
#[repr(C)]
struct BytePacket<T: ?Sized> {
version: u8,
flags: u8,
payload: T, // Last field may be unsized (`?Sized`)!
}
fn main() {
// Constructed with a sized `[u8; 4]` tail, then coerced to unsized `&BytePacket<[u8]>`:
let concrete: BytePacket<[u8; 4]> = BytePacket {
version: 1,
flags: 0x0F,
payload: [0xDE, 0xAD, 0xBE, 0xEF],
};
let dst_ref: &BytePacket<[u8]> = &concrete;
println!(
"v{}, payload len = {}, total DST size = {} bytes",
dst_ref.version,
dst_ref.payload.len(),
std::mem::size_of_val(dst_ref)
);
}
v1, payload len = 4, total DST size = 6 bytes