| |

Rust 72 🦀 Type Sizing: Sized vs Unsized Types (?Sized Bound)

⏱️ Estimated read: 17 min | |

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

BoundMeaningWhere
T: Sized (implicit)Size known at compile timeAll type parameters by default
T: ?SizedMay or may not be sizedOpt-out of the implicit bound
Self: ?Sized (implicit)Trait can be object-safeSelf in trait definitions

DSTs and Their Pointers

DSTPointer FormMetadata
str&str, Box<str>Length
[T]&[T], Box<[T]>Length
dyn Trait&dyn Trait, Box<dyn Trait>Vtable pointer

Where DSTs Can Appear

LocationAllowed?
Local variableNo
Function parameter by valueNo
Function parameter by referenceYes (&T with T: ?Sized)
Return type by valueNo
Return type by referenceYes
Struct field (last only)Yes (makes struct unsized)
Generic type argumentOnly with ?Sized bound

?Sized in Standard Library

ItemWhy ?Sized
Box<T>Can hold Box<str>, Box<dyn Trait>
Rc<T>Same
Arc<T>Same
DerefTarget may be unsized (String: Deref<Target = str>)
AsRefTarget 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

PitfallWhy It HappensFix
str cannot be storedSize not known at compile timeUse &str or String
Generic function rejects strImplicit T: Sized boundAdd ?Sized and use &T
Trait cannot be made into objectExplicit Sized boundRemove the bound or use where Self: Sized on methods
Box<dyn Trait> is 16 bytesFat pointer (data + vtable)Expected; not a bug
size_of::<T>() fails for DSTSize not known at compile timeUse 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

ItemValue
SizedCompile-time known size
Implicit boundAll type parameters are Sized by default
?SizedRemoves the implicit bound
Meaning of ?SizedMay or may not be Sized
DSTsstr, [T], dyn Trait
DST storageBehind a pointer only
Fat pointerPointer + metadata
Metadata for str/[T]Length
Metadata for dyn TraitVtable pointer
Self: ?SizedImplicit in traits, enables object safety

Key takeaways:

  • Sized is the default. Every type parameter has an implicit Sized bound. This is what allows the compiler to allocate stack space, pass arguments by value, and lay out struct fields .
  • ?Sized is 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 .
  • ?Sized does not mean “not Sized.” It means “may or may not be Sized.” Every type satisfies ?Sized; some types satisfy Sized and some do not .
  • DSTs are the reason ?Sized exists. str, [T], and dyn Trait are unsized, and generic code that wants to accept them must relax the Sized bound .
  • 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: ?Sized bound. This is what makes trait objects possible. An explicit Sized bound on a trait makes it non-object-safe .
  • Methods can opt out of Sized individually. where Self: Sized on 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.


Was this helpful?


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!