1. Symptoms
When the Rust compiler encounters error E0391, you will see a message similar to the following:
error[E0391]: the size for values of type `{type}` cannot be known at compilation time
--> src/main.rs:5:5
|
5 | dyn MyTrait,
| ^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `MyTrait` cannot be made into an object without a `?Sized` bound
The error typically appears when attempting to use a trait object in a context requiring a sized type. Common indicators include attempting to store dyn Trait directly in a struct field, using unsized types as function parameters without explicit bounds, or creating collections of trait objects without appropriate indirection.
Shell output examples:
error[E0391]: the size for values of type `str` cannot be known at compilation time
--> src/main.rs:3:12
|
3 | data: str,
| ^^ doesn't have a size known at compile-time
error[E0391]: the size for values of type `[T]` cannot be known at compilation time
--> src/lib.rs:7:8
|
7 | slice: [u32],
| ^^^ doesn't have a size known at compile-time
The compiler will highlight the exact location where the unsized type appears and provide a helpful note about adding a ?Sized bound to resolve the issue.
2. Root Cause
Rust’s memory layout requires that all types have a known size at compile time, except when explicitly marked as potentially unsized. This requirement exists because the compiler needs to know how much memory to allocate for values, how to pass them as function arguments, and how to manage their placement on the stack or heap.
Error E0391 occurs when you attempt to use a type whose size is not statically determinable. In Rust’s type system, several types fall into this category: trait objects like dyn Trait, slices like [T] or str, and arrays of unsized elements. These types are collectively referred to as dynamically sized types (DSTs) or unsized types.
The core issue arises because trait objects are composed of two pointers: a pointer to the data and a pointer to the vtable containing method implementations. When you try to use dyn Trait directly as a struct field, the compiler cannot determine the struct’s total size because the trait object’s size depends on the concrete type that implements it at runtime.
Similarly, slices lack a fixed size because their length is only known at runtime. The slice type [T] represents a sequence of T elements of unknown length, making its total size indeterminate. Without knowing the length, the compiler cannot calculate the memory requirements for storing the slice.
The compiler enforces this constraint by requiring all type parameters to implement the Sized trait by default. When you need to work with potentially unsized types, you must explicitly opt out by adding a ?Sized bound to your generic parameters, which signals to the compiler that the type may not have a known size at compile time.
3. Step-by-Step Fix
Fix 1: Add ?Sized Bound to Generic Parameters
When your function or struct needs to accept or store unsized types, add a ?Sized bound to the generic parameter.
Before:
fn process_data(data: str) {
println!("{}", data);
}
struct Container {
item: dyn Display,
}
After:
fn process_data(data: &str) {
println!("{}", data);
}
struct Container<T: ?Sized> {
item: T,
}
Fix 2: Use Indirection for Trait Objects in Structs
Store trait objects behind a pointer type that has a known size.
Before:
struct Handler {
callback: dyn Fn(i32),
}
After:
struct Handler {
callback: Box<dyn Fn(i32)>,
}
// or use Rc for single-threaded shared ownership
struct HandlerSingleThread {
callback: Rc<dyn Fn(i32)>,
}
Fix 3: Reference Unsized Types Explicitly
When passing unsized types to functions, use references.
Before:
fn print_slice(slice: [u32]) {
for item in slice.iter() {
println!("{}", item);
}
}
After:
fn print_slice(slice: &[u32]) {
for item in slice.iter() {
println!("{}", item);
}
}
Fix 4: Use Slices Through References or Boxes
Before:
struct Buffer {
data: [u8],
}
After:
struct Buffer {
data: Box<[u8]>,
}
// Or use a reference if the buffer is borrowed
struct BufferRef {
data: &[u8],
}
Fix 5: Provide Sized Bounds Explicitly
If you need to ensure a generic type is sized, explicitly require it.
Before:
fn clone_and_return<T>(value: T) -> T {
value
}
After:
fn clone_and_return<T: Sized>(value: T) -> T {
value
}
// Or if you need both sized and unsized, use two parameters
fn process_both<T: Sized, U: ?Sized>(sized: T, unsized: &U) {
// implementation
}
4. Verification
After applying the fixes, verify that error E0391 no longer appears by compiling your project:
cargo build
A successful build will produce no error messages. You should also run your test suite to ensure the changes don’t introduce regressions:
cargo test
For more comprehensive verification, especially with complex generic code, run the clippy linter:
cargo clippy
Clippy may identify additional issues with your type usage and suggest further improvements. If you modified public API functions, consider running cargo doc to ensure documentation still builds correctly:
cargo doc --no-deps
Pay special attention to any warnings about type bounds, as these may indicate that your fix needs adjustment for downstream users of your code.
5. Common Pitfalls
When working with E0391, developers frequently encounter several recurring mistakes that can extend debugging time.
Forgetting that Sized is the default bound causes confusion when adding explicit bounds. In Rust, all generic type parameters implicitly require Sized unless you explicitly write ?Sized. Newcomers often write T: Sized expecting to enable something, not realizing this is already the default behavior.
Placing unsized types directly in structs without indirection is a common structural mistake. Many developers attempt to store dyn Trait directly as a field, not understanding that this requires knowing the total struct size at compile time. The solution is to wrap unsized types in some pointer type like Box, Rc, or &.
Confusing the type with a reference to the type leads to incorrect fixes. When you see [u32] in error messages, the correct approach is usually to use &[u32] or Box<[u32]>, not to try to make the array itself sized.
Overusing Box introduces unnecessary heap allocation overhead when a trait object is not actually needed. Sometimes a generic function with an appropriate trait bound is more efficient than dynamic dispatch.
Neglecting the effect on struct layouts when adding Box<dyn Trait> can change memory consumption significantly. A single pointer adds 8 bytes on 64-bit systems, but the actual trait object may be larger. Profile memory usage if this becomes relevant for your use case.
Forgetting about auto-deref behavior when passing references to functions expecting unsized types. Rust will automatically dereference references, so &str satisfies ?Sized bounds, but you cannot pass str directly without wrapping it.
6. Related Errors
E0277: the trait bound X: Trait is not satisfied frequently accompanies E0391 when attempting to use dynamically sized types with trait constraints. This error occurs because the compiler cannot verify that your type implements the required trait when the type’s size is unknown. Solving both errors often requires either adding proper trait bounds or wrapping the type appropriately.
E0038: the trait Trait cannot be made into an object arises when attempting to create a trait object from a trait that contains methods incompatible with dynamic dispatch, such as methods with generic parameters or associated constants. This error is closely related to E0391 because both involve limitations in how trait objects can be constructed, and understanding E0038 helps clarify when dynamic dispatch is appropriate versus when generics are needed.
E0605: operations on fat pointers relates to errors when working with trait objects because trait objects are essentially fat pointers containing both the data pointer and the vtable pointer. Misunderstanding how these pointers work can lead to attempts to manipulate them in ways that violate Rust’s safety guarantees, which the compiler catches with this error.