Advanced Type Features

Unsized Types

An unsized type (also called a Dynamically Sized Type or DST) is a type whose byte size is not fixed at compile time and does not implement the Sized marker trait (!Sized). Because the compiler cannot compute a fixed stack frame offset for an unsized value, unsized types can only be manipulated behind a pointer or reference (&T, &mut T, Box<T>, Rc<T>, Arc<T>, *const T, *mut T).

The Four Categories of Unsized Types in Rust

Unsized Type Category Syntax Pointer Metadata Stored in Fat Pointer
Slice [T] Element count (len: usize)
UTF-8 String Slice str Byte length (len: usize)
Trait Object dyn Trait Vtable pointer (*const VTable)
Custom Tail-Unsized Struct struct Packet<T: ?Sized> { header: u32, payload: T } Metadata of the final field T (len or vtable)

Object-Safety Rules for Placing Values Directly on the Stack (E0277)

Rust enforces four structural rules on unsized types:

  1. Local variables, function parameters, and return values must be Sized (let x: str fails with E0277).
  2. Array elements [T; N], tuple elements (A, B), and enum variant fields must be Sized.
  3. Only the last field of a struct may be an unsized type, and at most one field in a struct may be unsized.
  4. You cannot dereference and move an unsized value by value (let s = *boxed_slice; is forbidden).
Custom Tail-Unsized Struct (`Frame<[u8]>`):

  Fat Pointer `&Frame<[u8]>` (16 bytes on 64-bit)
  +-------------------+-------------------+
  | data_ptr          | len = 5           |
  +---------+---------+-------------------+
            |
            v
  Contiguous Memory Allocation (4 + 3 + 1 + 5 = 13 bytes, padded to align 4 = 16 bytes):
  +-------------------+---------------+-------+---+---+---+---+---+-----------+
  | sequence: u32 (4B)| flags: u8 (1B)|pad(3B)| 1 | 2 | 3 | 4 | 5 |  pad (3B) |
  +-------------------+---------------+-------+---+---+---+---+---+-----------+
  |<--- Sized prefix fields ----------------->|<-- Unsized tail `[u8]` ------>|

Defining and Constructing a Custom Tail-Unsized Struct

To define a struct whose final field can be unsized, declare the struct with a generic parameter T: ?Sized as its final field. You construct an instance using a sized tail type (such as [u8; 4]) and then unsize-coerce a pointer (&Frame<[u8; 4]> or Box<Frame<[u8; 4]>>) into the unsized form (&Frame<[u8]> or Box<Frame<[u8]>>):

#[derive(Debug)]
struct Frame<Payload: ?Sized> {
    sequence: u32,
    // Only the LAST field of a struct can be `?Sized`:
    payload: Payload,
}

fn inspect_frame(frame: &Frame<[u8]>) {
    println!(
        "seq = {}, payload len = {}, bytes = {:?}, total size_of_val = {} B",
        frame.sequence,
        frame.payload.len(),
        &frame.payload,
        size_of_val(frame)
    );
}

fn main() {
    // 1. Construct with a Sized array `[u8; 5]`:
    let sized_frame: Frame<[u8; 5]> = Frame {
        sequence: 42,
        payload: [10, 20, 30, 40, 50],
    };

    // 2. Coerce `&Frame<[u8; 5]>` to the unsized `&Frame<[u8]>`:
    let unsized_ref: &Frame<[u8]> = &sized_frame;
    inspect_frame(unsized_ref);

    // 3. Or allocate on the heap and coerce `Box<Frame<[u8; 3]>>` to `Box<Frame<[u8]>>`:
    let boxed_unsized: Box<Frame<[u8]>> = Box::new(Frame {
        sequence: 99,
        payload: [1, 2, 3],
    });
    inspect_frame(&boxed_unsized);
}
seq = 42, payload len = 5, bytes = [10, 20, 30, 40, 50], total size_of_val = 12 B
seq = 99, payload len = 3, bytes = [1, 2, 3], total size_of_val = 8 B