Type System Fundamentals

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