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:
- Local variables, function parameters, and return values must be
Sized(let x: strfails withE0277). - Array elements
[T; N], tuple elements(A, B), and enum variant fields must beSized. - Only the last field of a
structmay be an unsized type, and at most one field in a struct may be unsized. - 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