Rust 72 🦀 Type Sizing: Sized vs Unsized Types (?Sized Bound)
Rust’s type system requires that every value have a size known at compile time. This is not an arbitrary restriction—it is what allows the compiler to allocate stack space, pass arguments by value, and lay out struct fields. The Sized marker trait is how the compiler encodes this requirement . Every type parameter in a generic function or struct has an implicit Sized bound, which means fn foo<T>(x: T) can only accept types whose size is known at compile time. The ?Sized syntax is the escape hatch: it removes the implicit bound and allows the function to accept dynamically sized types (DSTs), provided the value is accessed through a pointer .
The two major DSTs in Rust are slices ([T], str) and trait objects (dyn Trait). Neither has a statically known size, so neither can be stored directly in a variable or passed by value. A pointer to a DST becomes a “fat pointer”—a wide pointer that carries additional metadata alongside the address. For a slice, the metadata is the length. For a trait object, the metadata is a vtable pointer . The ?Sized bound is what allows generic code to work with these types behind references, which is why it appears in the definition of str, [T], and every trait object.
Key point: Sized is an implicit bound on all type parameters, meaning they must have a compile-time known size. ?Sized removes that bound, allowing a type parameter to be either sized or unsized. Unsized types can only be used behind a pointer, and the pointer becomes a fat pointer that carries the metadata needed to work with the value.
Why Sized is the default and ?Sized is the exception
The stack allocation problem. Every local variable in Rust is allocated on the stack, and the compiler needs to know how many bytes to reserve. A type like i32 has a known size of 4 bytes, so the compiler can reserve exactly 4 bytes. A type like str has no known size—it could be any length—so the compiler cannot reserve space for it. The Sized bound enforces this requirement: if a type is not Sized, it cannot be stored in a local variable, passed by value, or returned from a function . This is why let s: str = "hello"; is a compile error and let s: &str = "hello"; is not.
The generic problem. A generic function fn foo<T>(x: T) is compiled once and then monomorphized for each concrete T. The compiler needs to know how to pass x—whether it goes in a register, on the stack, or by reference. Without a known size, the calling convention cannot be determined. The implicit Sized bound on T is what makes the generic function compilable. fn foo<T: ?Sized>(x: &T) works because the parameter is a reference, and a reference to a DST is a fat pointer with a known size (two words on most platforms) .
The trait object exception. The Self type in a trait definition is the one place where the implicit Sized bound does not apply. A trait like trait Draw { fn draw(&self); } has an implicit Self: ?Sized bound, which is what allows dyn Draw to exist. If the trait required Self: Sized, it could not be turned into a trait object, because the whole point of a trait object is to erase the concrete type and work with any implementor regardless of size .
The pointer completion problem. A pointer to a DST is not a single word. On a 64-bit platform, &u8 is 8 bytes (a single address), but &str is 16 bytes (an address and a length), and &dyn Draw is 16 bytes (an address and a vtable pointer). The pointer is “completed” by the metadata that makes the DST usable. The ?Sized bound is what allows the generic code to accept these pointer types without requiring the pointee to be sized .
a. The Sized trait and the implicit bound
The Sized trait is a marker trait with no methods. It is implemented automatically by the compiler for every type whose size is known at compile time. You cannot implement it manually for a type that is not sized, and you cannot opt out of it for a sized type. It is a compiler-enforced property, not a user-defined one .
// A generic function with the implicit Sized bound
// T must have a compile-time known size
fn takes_sized<T>(value: T) {
// T is stored on the stack, so it must be Sized
println!("Value is on the stack");
}
// This compiles because i32 is Sized
takes_sized(42);
// A struct with a generic parameter
// The implicit Sized bound applies to T
struct Foo<T>(T);
// This compiles because [i32; 3] is Sized
let _ = Foo([1, 2, 3]);
// This does not compile because [i32] is not Sized
// let _ = Foo([1, 2, 3][..]); // error: the size for values of type `[i32]` cannot be known
The Foo<T> example is the same one used in the standard library documentation for Sized . The array [i32; 3] has a known size (12 bytes on most platforms), so it satisfies the Sized bound. The slice [i32] has no known size, so it fails the bound. The error message is precise: “the size for values of type [i32] cannot be known at compilation time.”
The Sized bound is not just a restriction on storage. It also affects what the compiler can do with the type: it can be moved, copied, dropped without a custom destructor, and passed by value. An unsized type cannot be moved or copied because the compiler does not know how many bytes to copy. This is why the only way to use an unsized type is through a pointer, and the pointer itself is sized .
b. The ?Sized bound: relaxing the default
The ?Sized syntax is a “relaxed bound” or “widening bound.” It does not mean “not Sized“—it means “may or may not be Sized.” Every type is either Sized or not, and ?Sized accepts both . The question mark is special to Sized; you cannot write ?Clone or ?Debug to mean “may or may not implement Clone.”
// A generic function that accepts both sized and unsized types
// The ?Sized bound removes the implicit Sized requirement
// The value must be behind a pointer because unsized types cannot be passed by value
fn print_value<T: ?Sized + std::fmt::Display>(value: &T) {
// value is a reference to T, so T may be unsized
// Display provides the to_string method
println!("{}", value);
}
fn main() {
// Passing a sized type: i32 implements Display
print_value(&42);
// Passing an unsized type: str implements Display
// &"hello" is &str, which is a fat pointer
print_value("hello");
// Passing a trait object: dyn Display implements Display
let boxed: Box<dyn std::fmt::Display> = Box::new(42);
print_value(&*boxed);
}
The print_value function has T: ?Sized + Display, which means T can be i32 (sized), str (unsized), or dyn Display (unsized). The parameter is &T, which is a reference. A reference to a sized type is a thin pointer; a reference to an unsized type is a fat pointer. The function does not care which, because it only uses the Display implementation, which works through the reference .
The same pattern appears in the standard library’s str and [T] types. The str type is defined as an unsized type, and &str is a fat pointer containing an address and a length. The String type is a wrapper around a Vec<u8> that can be coerced into a Box<str> or a &str. The ?Sized bound is what allows String to implement Deref<Target = str>, which is why you can call str methods on a String .
c. The Self exception and trait object compatibility
The one place where the Sized bound does not apply by default is the Self type in a trait definition. This is a deliberate exception: if every trait required Self: Sized, no trait could be turned into a trait object, because the trait object would have to know the size of the erased type .
// A trait with the implicit Self: ?Sized bound
// This trait can be turned into a trait object
trait Draw {
fn draw(&self);
}
// A trait that explicitly requires Self: Sized
// This trait cannot be turned into a trait object
trait Factory: Sized {
fn create() -> Self;
}
// This compiles: Draw is object-safe
let drawable: &dyn Draw = &Button;
// This does not compile: Factory is not object-safe
// let factory: &dyn Factory = &Button; // error: the trait `Factory` cannot be made into an object
The Factory trait has a method create() -> Self. This method returns Self, which requires Self to be Sized because the return type must have a known size. The explicit Sized bound on the trait makes the trait non-object-safe. The Draw trait has no such requirement, so it is object-safe .
The distinction between a trait-level Sized bound and a method-level Sized bound is important. A trait can be object-safe even if some of its methods require Self: Sized, as long as those methods are not called through the trait object. The standard library’s Iterator trait is the canonical example: it has many methods like map and filter that require Self: Sized, but the next method does not, so dyn Iterator is valid .
// A trait that is object-safe despite having a Sized method
trait MyIterator {
// This method is callable through a trait object
fn next(&mut self) -> Option<i32>;
// This method requires Self: Sized, so it is not callable through a trait object
// but the trait itself remains object-safe
fn count(self) -> usize where Self: Sized {
let mut count = 0;
let mut this = self;
while this.next().is_some() {
count += 1;
}
count
}
}
The where Self: Sized bound on count excludes it from the vtable, but does not make the entire trait non-object-safe. The next method remains callable through dyn MyIterator, and the count method remains callable on concrete types .
Complete Example Session
The following session builds a function that accepts both sized and unsized types, demonstrates the difference between T and &T, and shows why the ?Sized bound is necessary for generic code that works with str, slices, and trait objects.
// ============================================
// PART 1: THE SIZED DEFAULT
// ============================================
// This function has the implicit T: Sized bound
// It accepts values that have a known size at compile time
fn takes_owned<T>(value: T) -> String {
// The value is moved into the function
// It is stored on the stack, so T must be Sized
format!("Received a sized value")
}
// ============================================
// PART 2: THE ?SIZED RELAXATION
// ============================================
// This function removes the implicit Sized bound
// It accepts both sized and unsized types, but only behind a reference
fn takes_reference<T: ?Sized>(value: &T) -> String {
// The value is a reference
// If T is unsized, the reference is a fat pointer
// The function only needs the reference, not the size
format!("Received a reference")
}
// ============================================
// PART 3: DEMONSTRATING THE DIFFERENCE
// ============================================
fn main() {
// Sized types work with both functions
takes_owned(42);
takes_reference(&42);
// Unsized types only work with the reference function
let text: &str = "hello";
// takes_owned(*text); // error: str is not Sized
takes_reference(text); // OK: &str is a fat pointer
// Unsized slices only work with the reference function
let numbers: &[i32] = &[1, 2, 3];
// takes_owned(*numbers); // error: [i32] is not Sized
takes_reference(numbers); // OK: &[i32] is a fat pointer
// Trait objects only work with the reference function
let boxed: Box<dyn std::fmt::Display> = Box::new(42);
// takes_owned(*boxed); // error: dyn Display is not Sized
takes_reference(&*boxed); // OK: &dyn Display is a fat pointer
}
// ============================================
// PART 4: THE DISPLAY BOUND WITH ?SIZED
// ============================================
// A function that accepts any Display value behind a reference
// The ?Sized bound is necessary because str and dyn Display are unsized
fn print_display<T: ?Sized + std::fmt::Display>(value: &T) {
// The Display implementation works through the reference
// It does not need to know the size of T
println!("{}", value);
}
fn demonstrate_display() {
// Sized: i32
print_display(&42);
// Unsized: str
print_display("hello");
// Unsized: dyn Display
let boxed: Box<dyn std::fmt::Display> = Box::new("trait object");
print_display(&*boxed);
}
The four parts cover the implicit Sized bound on owned values, the ?Sized relaxation on references, the demonstration that unsized types only work through references, and the Display bound that accepts both sized and unsized types. What the session demonstrates is that the Sized bound is about storage and passing by value, not about capabilities—a str can implement Display just like an i32, but it cannot be stored in a local variable without a reference.
Quick Reference
Sized and ?Sized
| Bound | Meaning | Where |
|---|---|---|
T: Sized (implicit) | Size known at compile time | All type parameters by default |
T: ?Sized | May or may not be sized | Opt-out of the implicit bound |
Self: ?Sized (implicit) | Trait can be object-safe | Self in trait definitions |
DSTs and Their Pointers
| DST | Pointer Form | Metadata |
|---|---|---|
str | &str, Box<str> | Length |
[T] | &[T], Box<[T]> | Length |
dyn Trait | &dyn Trait, Box<dyn Trait> | Vtable pointer |
Where DSTs Can Appear
| Location | Allowed? |
|---|---|
| Local variable | No |
| Function parameter by value | No |
| Function parameter by reference | Yes (&T with T: ?Sized) |
| Return type by value | No |
| Return type by reference | Yes |
| Struct field (last only) | Yes (makes struct unsized) |
| Generic type argument | Only with ?Sized bound |
?Sized in Standard Library
| Item | Why ?Sized |
|---|---|
Box<T> | Can hold Box<str>, Box<dyn Trait> |
Rc<T> | Same |
Arc<T> | Same |
Deref | Target may be unsized (String: Deref<Target = str>) |
AsRef | Target may be unsized |
Best Practices
✅ Do This:
// Accept unsized types behind a reference
fn print<T: ?Sized + Display>(value: &T) { println!("{}", value); }
// Use ?Sized when the function does not need the size
fn len<T: ?Sized>(value: &T) -> usize { std::mem::size_of_val(value) }
// Use Box<str> for owned unsized data
let owned: Box<str> = String::from("hello").into_boxed_str();
❌ Don’t Do This:
// Try to store an unsized type directly
let text: str = "hello"; // error: size not known
// Try to return an unsized type by value
fn make_str() -> str { "hello" } // error: size not known
// Add an explicit Sized bound to a trait that needs object safety
trait Draw: Sized { fn draw(&self); } // dyn Draw is impossible
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
str cannot be stored | Size not known at compile time | Use &str or String |
Generic function rejects str | Implicit T: Sized bound | Add ?Sized and use &T |
| Trait cannot be made into object | Explicit Sized bound | Remove the bound or use where Self: Sized on methods |
Box<dyn Trait> is 16 bytes | Fat pointer (data + vtable) | Expected; not a bug |
size_of::<T>() fails for DST | Size not known at compile time | Use size_of_val(&value) |
Real-World Examples
1. Accept str and String
fn print<T: ?Sized + Display>(value: &T) { println!("{}", value); }
print("hello"); // &str
print(&String::from("hello")); // &String
2. Accept slices
fn sum<T: ?Sized + AsRef<[i32]>>(values: &T) -> i32 {
values.as_ref().iter().sum()
}
3. Box<str> for owned text
let owned: Box<str> = "hello".into();
4. Box<[T]> for owned slices
let boxed: Box<[i32]> = vec![1, 2, 3].into_boxed_slice();
5. Trait object with ?Sized
fn call<T: ?Sized + Draw>(value: &T) { value.draw(); }
6. Deref implementation
impl Deref for MyString {
type Target = str; // str is unsized
fn deref(&self) -> &str { &self.0 }
}
7. Object-safe trait
trait Draw { fn draw(&self); } // implicit Self: ?Sized
8. Sized method on object-safe trait
trait Draw {
fn draw(&self);
fn clone_box(&self) -> Box<dyn Draw> where Self: Sized + Clone;
}
9. size_of_val for DST
let text: &str = "hello";
println!("{}", std::mem::size_of_val(text)); // 5
10. Generic with ?Sized and Display
fn log<T: ?Sized + Debug>(value: &T) { eprintln!("{:?}", value); }
Visual
Sized vs Unsized
┌──────────────────────────────────────────────┐
│ SIZED (i32, String, [u8; 4]) │
│ Size known at compile time │
│ Can be stored on stack │
│ Can be passed by value │
│ │ │
│ ▼ │
│ let x: i32 = 42; │
│ │
│ UNSIZED (str, [u8], dyn Trait) │
│ Size not known at compile time │
│ Must be behind a pointer │
│ Cannot be passed by value │
│ │ │
│ ▼ │
│ let x: &str = "hello"; │
└──────────────────────────────────────────────┘
Fat Pointers
┌──────────────────────────────────────────────┐
│ &u8 │
│ ┌─────────────────┐ │
│ │ address (8 bytes)│ thin pointer │
│ └─────────────────┘ │
│ │
│ &str │
│ ┌─────────────────┬─────────────────┐ │
│ │ address │ length │ │
│ └─────────────────┴─────────────────┘ │
│ fat pointer (16 bytes) │
│ │
│ &dyn Trait │
│ ┌─────────────────┬─────────────────┐ │
│ │ address │ vtable pointer │ │
│ └─────────────────┴─────────────────┘ │
│ fat pointer (16 bytes) │
└──────────────────────────────────────────────┘
The ?Sized Bound
┌──────────────────────────────────────────────┐
│ fn foo<T>(x: T) │
│ → implicit T: Sized │
│ → x is stored on the stack │
│ → rejects str, [T], dyn Trait │
│ │
│ fn foo<T: ?Sized>(x: &T) │
│ → T may or may not be Sized │
│ → x is a reference │
│ → accepts str, [T], dyn Trait │
└──────────────────────────────────────────────┘
Trait Object Safety
┌──────────────────────────────────────────────┐
│ trait Draw { fn draw(&self); } │
│ → implicit Self: ?Sized │
│ → dyn Draw is valid │
│ │
│ trait Factory: Sized { fn create() -> Self; }│
│ → explicit Self: Sized │
│ → dyn Factory is invalid │
│ → error: cannot be made into an object │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
Sized | Compile-time known size |
| Implicit bound | All type parameters are Sized by default |
?Sized | Removes the implicit bound |
Meaning of ?Sized | May or may not be Sized |
| DSTs | str, [T], dyn Trait |
| DST storage | Behind a pointer only |
| Fat pointer | Pointer + metadata |
Metadata for str/[T] | Length |
Metadata for dyn Trait | Vtable pointer |
Self: ?Sized | Implicit in traits, enables object safety |
Key takeaways:
Sizedis the default. Every type parameter has an implicitSizedbound. This is what allows the compiler to allocate stack space, pass arguments by value, and lay out struct fields .?Sizedis the exception. It removes the implicit bound, allowing a type parameter to be either sized or unsized. The value must be behind a pointer because unsized types cannot be stored or passed by value .?Sizeddoes not mean “not Sized.” It means “may or may not be Sized.” Every type satisfies?Sized; some types satisfySizedand some do not .- DSTs are the reason
?Sizedexists.str,[T], anddyn Traitare unsized, and generic code that wants to accept them must relax theSizedbound . - Fat pointers carry the metadata. A pointer to a DST is two words: an address and the information needed to use the value (length for slices, vtable for trait objects) .
- Traits have an implicit
Self: ?Sizedbound. This is what makes trait objects possible. An explicitSizedbound on a trait makes it non-object-safe . - Methods can opt out of
Sizedindividually.where Self: Sizedon a method excludes it from the vtable while keeping the trait object-safe for its other methods .
Remember: Sized and ?Sized are about the compiler’s need to know how many bytes a value occupies. The Sized bound is the default because the compiler needs that information to allocate, move, and pass values. The ?Sized bound is the exception for the cases where the value is behind a pointer—the pointer is sized, and the metadata in the fat pointer completes the type. str, [T], and dyn Trait are the canonical unsized types, and the ?Sized bound is what allows generic code to accept them. The trait Self type is the one place where the bound is relaxed by default, because without that relaxation, trait objects could not exist at all.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!