When the compiler doesn't know the size
You write a generic function to print any value. You pass a Vec. It works. You pass a slice. The compiler rejects you with E0277 (the trait bound [i32]: Sized is not satisfied). You didn't change the logic. You only changed the type. The error points to a trait you've never seen: Sized.
This isn't a bug. It's Rust protecting you from a fundamental constraint of memory. The compiler needs to know how much space to reserve for variables. Most types have a fixed size. An i32 is always 4 bytes. A String is always 24 bytes. The compiler can calculate this at compile time. Some types don't. A slice [i32] could contain one element or a billion. The compiler can't know the size until the program runs.
Sized is the marker that tells the compiler "this type has a known size." Unsized types break that promise. They require special handling. Understanding Sized explains why slices and trait objects always live behind pointers, and why your generic code sometimes refuses to compile.
Sized vs Unsized
Rust divides types into two camps. Sized types have a fixed size known at compile time. Unsized types do not. The language calls unsized types DSTs, or Dynamically Sized Types.
Think of a moving truck. You can load a box labeled "Laptop" because you know it fits in a specific spot. You can't load a box labeled "All the water in the ocean" because you don't know how much space it takes. The truck driver (the stack allocator) needs to reserve space before the move starts. If the size is unknown, the driver can't do the job.
Sized types are the laptop. You can put them on the stack. You can pass them by value. You can return them by value. Unsized types are the ocean. You can't put the ocean on the stack. You can only interact with it through a hose. In Rust, the hose is a pointer. Unsized types must always be behind a reference &T, a box Box<T>, or another smart pointer. The pointer itself is sized. The data behind it is not.
Common unsized types include:
- Slices:
[T]. The length is dynamic. - Strings:
str. The length is dynamic. - Trait objects:
dyn Trait. The concrete type is dynamic.
Note the distinction. [T] is unsized. Vec<T> is sized. str is unsized. String is sized. dyn Trait is unsized. Box<dyn Trait> is sized. The wrapper provides the size. The inner type does not.
The default assumption
When you write a generic function, Rust assumes the type parameter is sized. This is the default bound.
/// Prints a value. T must be Sized by default.
fn print_value<T>(value: T) {
println!("{:?}", value);
}
fn main() {
let v = vec![1, 2, 3];
print_value(v); // Works. Vec is Sized.
let slice = [1, 2, 3];
// print_value(slice); // Error: the trait bound [i32]: Sized is not satisfied.
}
The compiler inserts a hidden bound T: Sized into every generic definition. This allows the compiler to generate code that places value on the stack with a known offset. If you try to use an unsized type, the compiler stops. It can't generate the call frame.
This default keeps 95% of generic code simple. Most functions work with concrete types or sized wrappers. You rarely need unsized types in application logic. The assumption holds until you hit slices or trait objects.
Opting out with ?Sized
You can relax the assumption using the ?Sized bound. The question mark means "Sized is optional." This tells the compiler to stop requiring a known size for T.
/// Accepts any type, sized or unsized.
/// T can be a slice, a trait object, or a concrete type.
fn print_any<T: ?Sized>(value: &T) {
println!("{:?}", value);
}
fn main() {
let v = vec![1, 2, 3];
print_any(&v); // Works. Vec is Sized.
let slice = [1, 2, 3];
print_any(&slice); // Works. [i32] is unsized, but &T accepts it.
let obj: &dyn std::fmt::Debug = &42;
print_any(obj); // Works. dyn Debug is unsized.
}
Adding ?Sized removes the requirement. The function now accepts unsized types. Notice the parameter is &T, not T. You still can't pass an unsized type by value. The reference &T is sized. The data behind it can be anything.
The ?Sized syntax is an opt-out. You use it when you need a generic to work with trait objects or raw slices. It's common in library code that builds adapters or formatting utilities. Application code rarely needs it.
How unsized types survive
Unsized types exist behind fat pointers. A fat pointer is a pointer that carries extra metadata. The metadata gives the pointer a fixed size, even though the data doesn't.
A reference to a slice &[T] is a fat pointer. It contains two parts:
- A pointer to the first element.
- The length of the slice.
The reference itself takes two machine words. The compiler knows this size. It can put the reference on the stack. The slice data lives elsewhere, usually on the heap or in static memory. The length tells the program how many elements to process.
A reference to a trait object &dyn Trait is also a fat pointer. It contains:
- A pointer to the concrete data.
- A pointer to the vtable (virtual method table).
The vtable holds function pointers for the trait methods. It allows dynamic dispatch. The reference has a fixed size. The concrete type can be anything that implements the trait.
This explains why &T works with ?Sized. The reference is always sized. The metadata makes it so. The compiler can allocate space for the reference. It doesn't need to know the size of the data.
Real-world: Generic debugging
A common use case for ?Sized is a generic function that needs to accept trait objects. Trait objects are unsized. Without ?Sized, the generic rejects them.
use std::fmt::Debug;
/// Logs a value using Debug.
/// Accepts concrete types, slices, and trait objects.
fn log_value<T: Debug + ?Sized>(label: &str, value: &T) {
println!("[{}] {:?}", label, value);
}
fn main() {
let v = vec![1, 2, 3];
log_value("vec", &v);
let slice = [1, 2, 3];
log_value("slice", &slice);
// Trait object. dyn Debug is unsized.
let obj: &dyn Debug = &"hello";
log_value("object", obj);
}
The Debug trait itself is defined with ?Sized. This allows implementations for unsized types like str and [T]. If Debug required Sized, you couldn't print a slice or a string slice directly. The trait definition would block it.
Most traits are Sized by default. Clone is Sized. You cannot create dyn Clone. You cannot clone a trait object directly. Debug is ?Sized. You can create dyn Debug. You can debug a trait object. The trait author decides this. When you write your own traits, they default to Sized. Add ?Sized if you want trait objects.
/// A trait that allows trait objects.
trait MyTrait: ?Sized {
fn do_something(&self);
}
fn main() {
// This compiles because MyTrait is ?Sized.
let obj: &dyn MyTrait;
// ...
}
Pitfalls and errors
The compiler enforces Sized strictly. Violations produce clear errors.
E0277: the trait bound T: Sized is not satisfied
This error appears when you use an unsized type in a context that requires Sized. It happens with generic defaults, return types, and struct fields.
fn bad_return<T: ?Sized>() -> T {
// Error: the size for values of type T cannot be known at compilation time
unimplemented!()
}
You can't return an unsized type by value. The function signature promises to return T. The compiler needs to know how much space to reserve for the return value. If T is unsized, it can't. You must return a pointer. Change the return type to &T, Box<T>, or Arc<T>.
Struct fields must be sized
Structs store data inline. Every field must have a known size. You can't put an unsized type directly in a struct.
struct Bad {
// Error: the size for values of type [i32] cannot be known at compilation time
data: [i32],
}
struct Good {
// Box makes the field sized. The data is unsized, but the box is sized.
data: Box<[i32]>,
}
Use a pointer wrapper for unsized fields. Box<T> is the standard choice. It allocates the data on the heap and stores a fat pointer in the struct. The struct size is fixed.
impl Trait is always Sized
The impl Trait syntax is syntactic sugar for generics. It is always sized. You can't return impl Trait that is unsized. You must use dyn Trait.
fn get_debug() -> impl std::fmt::Debug {
// Error: impl Trait cannot be unsized
// "hello" is &str, which is unsized.
// Actually "hello" is &str, which is sized? No, &str is sized.
// Let's use a trait object.
let obj: &dyn std::fmt::Debug = &42;
obj
}
impl Trait hides a concrete type. The concrete type must be sized. If you need dynamic dispatch, use dyn Trait. The syntax is different for a reason. impl Trait is static dispatch. dyn Trait is dynamic dispatch.
Decision matrix
Use plain T when you need the value on the stack or you want to enforce that callers provide concrete types with known sizes. This covers most generic functions and structs. The default Sized bound keeps code predictable and efficient.
Use T: ?Sized when your function needs to accept trait objects like dyn Trait or slices [T] behind a reference. This is rare outside of library code writing generic adapters or formatting utilities. Only add it when the compiler forces your hand.
Use Box<T> when you need to own an unsized type. The box holds a fat pointer, making the handle sized while the data remains unsized. This is the standard way to store slices or trait objects in structs.
Use &T when you only need to read an unsized type and don't care about ownership. References are the most common way to interact with unsized data. They are cheap and safe.
Convention: Write ?Sized only when necessary. It makes the generic harder to reason about. If your function works with Vec and &[T], consider using impl AsRef<[T]> instead of ?Sized. It's more specific and often clearer.
Convention: dyn Trait is the standard syntax for trait objects. The old Trait syntax is deprecated. Always use dyn to mark trait objects. It signals that the type is unsized and uses dynamic dispatch.
Unsized types are ghosts. You can't hold them directly. You need a pointer to see them. Keep your generics Sized unless you're building the plumbing for trait objects.