What is the Sized trait

The Sized trait indicates a type has a known size at compile time, which is required for stack allocation and generic parameters by default.

The size wall

You write a helper function to log any value. You make it generic. You call it with a String. It works. You call it with a str. The compiler screams. You call it with a dyn Display. It screams louder. You didn't make a logic error. You hit a wall called Sized.

Rust is a systems language. It manages memory manually. To put a value on the stack, the compiler must know exactly how many bytes to allocate. The Sized trait is a promise: "My size is known at compile time." If a type implements Sized, you can store it directly in a variable, return it by value, or put it in a struct. If it doesn't, you can only hold it behind a pointer.

What Sized actually means

Every type in Rust falls into one of two camps. Sized types have a fixed size the compiler can calculate before the program runs. Integers, structs, enums, and vectors are sized. A u64 is always 8 bytes. A String is always 24 bytes (pointer, length, capacity). The compiler reserves that space and you're done.

Unsized types have no fixed size at compile time. The string slice str is unsized. A slice of unknown length could be 1 byte or 1 gigabyte. The trait object dyn Widget is unsized. The concrete type implementing Widget isn't known until runtime, so the size isn't known either.

You cannot store an unsized type directly. You can't have a variable of type str. You can't return dyn Widget from a function. You must wrap unsized types in a pointer. The pointer itself is sized. A &str is 16 bytes. A Box<dyn Widget> is 16 bytes. The pointer carries the size information at runtime.

/// A struct with a sized field.
struct Cache {
    // String is sized. The compiler knows it takes 24 bytes.
    // This struct is also sized.
    data: String,
}

// This struct won't compile.
// struct BadCache {
//     // str is unsized. The compiler doesn't know how big it is.
//     // You can't put an unknown-sized value directly in a struct.
//     data: str,
// }

The compiler enforces this rule strictly. It prevents you from allocating the wrong amount of memory or creating dangling references. The Sized trait is the mechanism that makes this enforcement possible.

The default bound trap

Here's the part that catches everyone. Every generic parameter in Rust carries a hidden bound. When you write fn foo<T>, the compiler reads it as fn foo<T: Sized>. This default keeps the language ergonomic. Most types you write are sized. The compiler assumes you want sized types unless you say otherwise.

This default causes the first error. You write a generic function that takes a reference. You think the reference handles the size. It doesn't.

/// This function looks like it accepts anything.
/// It actually requires T to be Sized.
fn log<T>(val: &T) {
    // The bound is on T, not on &T.
    // T defaults to Sized.
    println!("Logged");
}

fn main() {
    let s = String::from("hello");
    log(&s); // Works. String is Sized.

    let slice: &str = "world";
    // log(slice); // Error! str is not Sized.
}

The error happens because the bound is on T. The reference &T is sized, but the generic parameter T still defaults to Sized. When you pass &str, T becomes str. str does not implement Sized. The compiler rejects the call.

You must explicitly opt out of the default bound. The syntax ?Sized means "Sized is optional." It relaxes the constraint on T.

/// This function accepts both sized and unsized types.
fn log_maybe_sized<T: ?Sized>(val: &T) {
    // T can be str, dyn Trait, or any sized type.
    // The reference &T is always sized, so it fits in the argument slot.
    println!("Logged");
}

fn main() {
    let s = String::from("hello");
    log_maybe_sized(&s); // Works.

    let slice: &str = "world";
    log_maybe_sized(slice); // Works. T is str, which is ?Sized.
}

The ? prefix is Rust's way of negating a bound. ?Sized is the only common use of this syntax. It tells the compiler to stop enforcing the size requirement for that parameter.

Why references don't save you automatically

The confusion around &T comes from fat pointers. A reference to a sized type is a thin pointer. It's just a memory address. A reference to an unsized type is a fat pointer. It carries extra data.

A &str is a fat pointer containing a data pointer and a length. A &dyn Widget is a fat pointer containing a data pointer and a vtable pointer. The reference itself is always sized. The compiler knows how big a fat pointer is. It's always two words.

The problem is the generic bound. The compiler checks the bound on T before it looks at the reference. If T must be Sized, the compiler rejects str immediately. It doesn't matter that you're wrapping str in a reference. The bound fails first.

This distinction matters when you write traits. You often see traits with methods that require Sized.

trait Widget {
    /// Draw the widget.
    fn draw(&self);

    /// Clone the widget. Requires Self to be Sized.
    fn clone_widget(&self) -> Self
    where
        Self: Sized;
}

The where Self: Sized clause restricts the method. You can call draw on a dyn Widget. You cannot call clone_widget on a dyn Widget. Cloning requires returning Self by value. If Self is unsized, you can't return it. The clause prevents the method from being generated for trait objects.

Convention aside: The community convention is to pair ?Sized with a reference or smart pointer. Writing fn foo<T: ?Sized>(t: T) is legal syntax but impossible to call, since you can't pass an unsized value by value. If you see ?Sized without a pointer type, check the signature carefully. It's usually a mistake.

Real code: Trait objects and slices

You encounter Sized most often when working with trait objects and slices. Trait objects allow dynamic dispatch. You can store different types behind the same interface. The trade-off is size. The concrete type isn't known at compile time.

trait Renderer {
    fn render(&self);
}

struct Canvas;
impl Renderer for Canvas {
    fn render(&self) { println!("Canvas"); }
}

struct Text;
impl Renderer for Text {
    fn render(&self) { println!("Text"); }
}

/// A collection of renderers.
struct Scene {
    // Vec requires sized elements.
    // Box<Renderer> is sized. The Box manages the unsized dyn Renderer.
    items: Vec<Box<dyn Renderer>>,
}

impl Scene {
    fn add(&mut self, item: Box<dyn Renderer>) {
        self.items.push(item);
    }
}

The Vec stores Box<dyn Renderer>. The Box is a smart pointer. It's sized. The dyn Renderer inside is unsized. The Box allocates the concrete type on the heap and holds a pointer to it. The Vec only sees the Box. It knows the size of the Box. Everything works.

Slices behave similarly. [T] is unsized. [T; N] is sized. A fixed-size array has a known length. A slice has a runtime length.

/// Process a slice of numbers.
fn sum<T: ?Sized>(slice: &T)
where
    T: AsRef<[i32]>
{
    // T can be [i32; 5] or [i32].
    // The reference &T is sized.
    let total: i32 = slice.as_ref().iter().sum();
    println!("Sum: {}", total);
}

fn main() {
    let arr = [1, 2, 3, 4, 5];
    sum(&arr); // Works. [i32; 5] is Sized.

    let vec = vec![1, 2, 3];
    sum(&vec); // Works. Vec coerces to &[i32].
}

The AsRef<[i32]> bound allows the function to accept arrays, vectors, and slices. The T: ?Sized bound allows T to be [i32]. Without ?Sized, the function would reject slices.

Pitfalls and compiler errors

The compiler gives you clear errors when you violate size rules. Learn to read them.

If you try to return a trait object by value, you get E0277.

trait Widget {
    fn draw(&self);
}

struct Button;
impl Widget for Button {
    fn draw(&self) {}
}

// This fails.
// fn create_widget() -> dyn Widget {
//     Button
// }
// Error: E0277: the size for values of type dyn Widget cannot be known at compilation time.

The error tells you exactly what's wrong. dyn Widget is unsized. You can't return it by value. Wrap it in a Box.

fn create_widget() -> Box<dyn Widget> {
    Box::new(Button)
}

If you try to use impl Trait with an unsized type, you get a similar error. impl Trait is always sized. It's a placeholder for a single concrete type. The compiler monomorphizes it. It knows the size.

// This fails.
// fn get_slice() -> impl AsRef<str> {
//     "hello"
// }
// Error: impl Trait cannot be used to return unsized types.

impl Trait works for sized types. dyn Trait works for unsized types. They solve different problems. Don't mix them up.

If you try to store an unsized type in a struct, the compiler rejects the struct definition.

// This fails.
// struct Data {
//     content: str,
// }
// Error: the size for values of type str cannot be known at compilation time.

Use String or &str or Box<str>. Pick the ownership model you need.

Convention aside: Use let _ = value; to discard a result when you intentionally ignore it. This signals to readers that you considered the value and chose to drop it. It also suppresses warnings. The compiler treats let _ as a deliberate discard.

Decision matrix

Use T (implicitly T: Sized) when you want to store the value directly, return it by value, or pass it by value. This covers the vast majority of your code. Structs, enums, integers, and vectors are sized. The compiler optimizes sized types aggressively.

Use T: ?Sized when you are writing a generic function that must accept both sized types and unsized types like str or dyn Trait. You almost always pair this with a reference, like &T or &mut T. The ?Sized bound relaxes the constraint on T, allowing unsized types to flow through the generic.

Use &T when you need to read or modify data without taking ownership, and the data might be unsized. References are fat pointers for unsized types, carrying the size information at runtime. The reference itself is sized, so it fits in arguments and return values.

Use Box<T> when you need to own an unsized value on the heap. Box is a smart pointer that knows how to manage the memory of a dyn Trait or a str. It provides a sized wrapper around an unsized payload.

Use impl Trait when you want to return a sized type without naming it. impl Trait is always sized. It enables monomorphization and inlining. You cannot use impl Trait for dynamic dispatch or unsized types.

Trust the compiler on size. If it says unknown, you need a pointer. Don't fight the size system. Wrap the value in a Box or a reference and move on.

Where to go next