Sized Trait
Understand the Sized marker trait, why Rust requires compile-time sizes, and how to work with dynamically sized types using ?Sized.
In Rust, every value must have a known size at compile time. This requirement is not optional; it is the foundation that allows the compiler to allocate stack space, move values around, and generate efficient code without runtime size lookups. The Sized trait is the marker that identifies types meeting this requirement. Most Rust types are Sized automatically, and you rarely have to think about it—until you encounter a dynamically sized type, at which point the rules become essential to understand.
What It Means for a Type to Be Sized
When a type is Sized, the compiler knows exactly how many bytes an instance of that type occupies. This is true for all primitive types, structs with fixed fields, enums (the compiler computes the size of the largest variant plus a discriminant), and even references. A reference is always the size of a pointer—typically 8 bytes on a 64‑bit platform—regardless of what it points to.
use std::mem::size_of;
fn main() {
println!("i32: {} bytes", size_of::<i32>()); // 4
println!("&i32: {} bytes", size_of::<&i32>()); // 8 (or 4 on 32‑bit)
println!("String: {} bytes", size_of::<String>()); // varies, but fixed
println!("&String: {} bytes", size_of::<&String>()); // 8
println!("[i32; 3]: {} bytes", size_of::<[i32; 3]>()); // 12
}
Everything in the example above is Sized. The compiler can allocate space for these values on the stack, pass them by value, and inline operations because the sizes never change.
References Are Always Sized:
A reference &T has a fixed pointer-sized layout even when T itself is unsized. This is why you can always have an &str or &dyn Trait in a local variable—the reference itself is Sized, even though the thing it points to may not be.
Dynamically Sized Types (DSTs)
A dynamically sized type (DST) cannot have its size known by looking at its definition alone. The size depends on runtime data or on the specific concrete type behind a trait object. The three main categories are:
- Slices (
[T]) — a contiguous sequence ofTwhose length is only known at runtime. - String slices (
str) — a valid UTF‑8 sequence whose byte length is runtime‑determined. - Trait objects (
dyn Trait) — a value that implements a trait, but whose concrete type is erased at compile time.
You can never hold a DST directly in a local variable:
// This will not compile: `str` is unsized.
let s: str = "hello";
The compiler error is explicit: the size for values of type str cannot be known at compilation time. DSTs must always live behind some kind of pointer—&str, Box<str>, Rc<str>, &dyn Display, Box<dyn Display>, etc. Those pointer types are themselves Sized, so you can put them on the stack.
The Sized Marker Trait
The std::marker::Sized trait is a marker trait with no methods and no associated items. It serves only to inform the type system that a type has a known size.
pub trait Sized {
// empty
}
Rust automatically implements Sized for every type whose size is fixed at compile time. You cannot implement Sized yourself—it is a compiler‑built‑in auto trait. Unsized types (DSTs) simply do not implement it.
You Cannot Implement Sized Manually:
Attempting to write impl Sized for MyType {} will produce a compiler error. This restriction exists because the compiler must be the sole authority on what constitutes a fixed size; allowing manual implementation would break memory safety guarantees.
The Implicit Sized Bound on Generics
Every generic type parameter in Rust comes with an implicit Sized bound. When you write fn foo<T>(x: T), Rust actually reads fn foo<T: Sized>(x: T). This shorthand makes sense most of the time, but it causes compilation failures when you intend to accept an unsized type behind a reference.
Consider a function that takes a reference to something Display and prints it:
use std::fmt::Display;
fn show<T: Display>(value: &T) {
println!("{}", value);
}
fn main() {
let s: &str = "hello";
show(s); // Error: `str` does not have a constant size known at compile-time
}
Here the type parameter T is inferred to be str (because s is &str). str implements Display, but it is not Sized, so the implicit T: Sized bound is violated. The error message may seem confusing because you are passing a reference, but the bound applies to T itself, not to the reference.
Using ?Sized to Accept Dynamically Sized Types
The syntax ?Sized relaxes the implicit bound. It means “T may or may not be Sized.” Once added, the function can accept an unsized type through a reference:
fn show<T: Display + ?Sized>(value: &T) {
println!("{}", value);
}
fn main() {
let s: &str = "hello";
show(s); // works: T = str, which is ?Sized
show(&42); // works: T = i32 (sized)
}
The same rule applies to Box and other smart pointers. Box<T> itself has a T: ?Sized bound in the standard library, which is why Box<dyn Display> and Box<str> are allowed. If Box required T: Sized, you could never put a trait object or slice on the heap.
Correct Use of ?Sized:
Adding ?Sized to your generic bounds when you only ever access T behind a pointer (&T, Box<T>, Rc<T>, etc.) is the idiomatic way to accept unsized types. Your function becomes more general without sacrificing any performance, because the pointer itself remains Sized.
Sized and Trait Objects
A trait object (dyn Trait) is itself an unsized type. You can never hold a dyn Trait as a plain local variable; you must put it behind a pointer. The interplay with Sized shows up in two important ways.
Trait objects for traits without a Sized bound
If a trait does not require Self: Sized, you can create a trait object from it:
trait Speak {
fn say(&self) -> &str;
}
struct Dog;
impl Speak for Dog {
fn say(&self) -> &str { "woof" }
}
fn main() {
let animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.say()); // woof
}
Box<dyn Speak> is Sized because Box is a pointer, even though dyn Speak is not.
A Sized supertrait blocks trait objects
If a trait has Sized as a supertrait, it cannot be used to form a trait object. This is by design: a trait object requires erasing the concrete type, and Sized demands that the concrete type be known.
trait Fixed: Sized {
fn method(&self) {}
}
// Error: `Fixed` cannot be made into an object
let obj: Box<dyn Fixed> = Box::new(());
The compiler rejects this because every implementor of Fixed must be Sized, so the trait was never intended to be used via dynamic dispatch. It's a common pattern to use trait MyTrait: Sized when you only intend the trait to be used with static dispatch (generics).
Object Safety Check:
The Sized bound is one of the reasons a trait can be non‑object‑safe. If you see the error “the trait Foo cannot be made into an object”, check whether Foo (or any of its supertraits) has a Sized requirement.
Practical Implications and Common Mistakes
1. Forgetting the implicit Sized bound
The most frequent error: a generic function accepts a reference but still fails because T must be Sized. The fix is T: ?Sized.
2. Trying to declare an unsized local variable
Variables on the stack must have a known size. Code like let x: dyn Display = ... will always fail. Use Box<dyn Display>, &dyn Display, or a similar wrapper.
3. Confusing pointer size with pointee size
A reference to an unsized type (&str, &dyn Trait) is itself Sized. The fact that you can store an &str in a local variable sometimes leads beginners to believe str itself is Sized, which it is not.
4. Overusing ?Sized
Adding ?Sized when you intend to accept only sized types can introduce unnecessary complexity or accidentally allow unsized types that you don't handle properly. If your function genuinely moves or owns a value of type T, it must be Sized.
5. Attempting to implement Sized manually
As noted, this is forbidden. If you need a type to always be Sized, simply ensure its fields have known sizes—the compiler will do the rest.
When You Don't Need to Worry About Sized
In everyday Rust code, Sized is invisible. You write structs, enums, functions, and generics, and the compiler enforces size constraints silently. You only need to think about Sized when:
- You read a compiler error containing “the size for values of type … cannot be known at compilation time.”
- You are writing a generic function that operates solely through references and you want it to work with unsized types like
stror[T]. - You are designing a trait and need to decide whether it should support trait objects (omit
Sized) or be limited to static dispatch (addSizedas a supertrait).
Outside these scenarios, the implicit Sized bound works perfectly and keeps your types predictable.
Summary
The Sized trait is the silent backbone of Rust's memory model. It ensures that the compiler knows exactly how much space a value occupies, which in turn enables stack allocation, efficient moves, and clean generic code generation through monomorphization. The ?Sized relaxation provides a controlled escape hatch, allowing pointers to unsized types while keeping the pointer itself firmly Sized.
This trait underpins many other utility traits you will encounter: Clone, Copy, and Default all require Sized because they need to produce or duplicate values with fixed footprints. Understanding Sized prepares you to work with these traits and to appreciate why Rust's approach to generics—static dispatch by default, dynamic dispatch only when requested—delivers predictable performance without sacrificing expressiveness.