Advanced Type Features

Sized

std::marker::Sized is the compiler-implemented marker trait for types with a constant byte size known at compile time, and ?Sized ("may or may not be Sized") is the relaxed bound syntax that opts a generic type parameter out of Rust's implicit T: Sized default.

Implicit Sized Bounds Across Rust Constructs

To keep everyday generic code ergonomic, Rust automatically adds an implicit Sized bound to almost every generic type parameter—with one important exception (trait Self):

Construct Written Syntax Implicit Desugared Bound Why
Generic function fn foo<T>(x: &T) fn foo<T: Sized>(x: &T) Allows local bindings and by-value returns of T by default
Generic struct / enum struct Box<T> struct Box<T: Sized> Struct fields must have known layout unless opted out on the tail field
Generic impl block impl<T> Wrapper<T> impl<T: Sized> Wrapper<T> Matches default struct bound
trait definition Self trait MyTrait { ... } trait MyTrait where Self: ?Sized No implicit Sized bound! If Self: Sized were added implicitly to traits, dyn MyTrait could never implement MyTrait!
Implicit Bound Comparison:
  fn process<T>(val: &T)       ===>  `T: Sized` is added implicitly!
                                     (Rejects `process("hello")` where `T = str`!)

  fn process<T: ?Sized>(val: &T) =>  `T` can be `Sized` OR unsized (`str`, `[U]`, `dyn Trait`)!

Widening Generic Functions with T: ?Sized

Whenever a generic function or struct only accesses T behind a pointer (&T, &mut T, Box<T>, Rc<T>, Arc<T>), adding T: ?Sized makes the API strictly more general without costing any performance on Sized types:

use std::fmt::Debug;

// Without `?Sized`, passing `"slice"` (`&str`) or `&[1, 2][..]` (`&[i32]`)
// would fail to compile because `str` and `[i32]` are `!Sized`:
fn log_inspected<T: Debug + ?Sized>(label: &str, value: &T) {
    println!(
        "[{label}] value = {:?} (size_of_val = {} bytes)",
        value,
        size_of_val(value)
    );
}

fn main() {
    let sized_num: u64 = 42;
    let unsized_str: &str = "dynamic string slice";
    let unsized_slice: &[i32] = &[10, 20, 30];
    let trait_obj: &dyn Debug = &("tuple", 99);

    log_inspected("u64", &sized_num);
    log_inspected("str", unsized_str);
    log_inspected("[i32]", unsized_slice);
    log_inspected("dyn Debug", trait_obj);
}
[u64] value = 42 (size_of_val = 8 bytes)
[str] value = "dynamic string slice" (size_of_val = 20 bytes)
[[i32]] value = [10, 20, 30] (size_of_val = 12 bytes)
[dyn Debug] value = ("tuple", 99) (size_of_val = 24 bytes)

Requiring Self: Sized on Specific Trait Methods vs. the Entire Trait

  1. trait Parse: Sized: Putting : Sized on the trait header requires every implementor to be Sized (for example, when methods return Result<Self, Error> by value) and permanently disables dyn Parse.
  2. fn into_boxed(self) -> Box<Self> where Self: Sized: Putting where Self: Sized on an individual trait method allows that method to take or return Self by value while keeping the rest of the trait usable as dyn Trait!