Rust 66 🦀 Object Safety Rules for Trait Objects
Trait objects are the mechanism that enables dynamic dispatch in Rust. A Box<dyn Trait> or &dyn Trait points to a concrete value and a vtable of function pointers, allowing method calls to be resolved at runtime without knowing the concrete type at compile time. But not every trait can become a trait object. The compiler enforces a set of rules collectively called object safety—or, in modern Rust terminology, dyn compatibility—that determine whether a trait is suitable for use behind a dyn pointer.
The reason these rules exist is structural. A vtable is a static table of function pointers, one per method. It cannot contain an infinite family of monomorphized generic functions, and it cannot describe a return type that depends on a Self the caller no longer knows. When the compiler rejects dyn Trait with error E0038, it is telling you that the trait’s shape is incompatible with runtime dispatch. The fix is usually either to mark the offending method where Self: Sized or to split the trait into a dispatchable core and a sized-only extension.
Key point: A trait is dyn-compatible if it does not require Self: Sized, has no associated constants, and every method intended for dispatch has no generic type parameters and uses Self only in the receiver position; methods that violate these rules can be opted out with where Self: Sized.
Why object safety rules exist
The vtable problem. A trait object is a fat pointer: one pointer to the data, one pointer to the vtable . The vtable contains a function pointer for each method the trait declares. If a method has a generic type parameter, the compiler would need to include a function pointer for every possible instantiation of that parameter—and there are infinitely many types in Rust. The vtable cannot represent that . This is why generic methods are not dispatchable.
The Self return problem. A method like fn clone(&self) -> Self is not dispatchable through dyn Trait. When you call x.clone() on a Box<dyn Trait>, what is the return type? The compiler has erased the concrete type, so it cannot know what Self is . The same problem applies to parameters: fn compare(&self, other: &Self) cannot be called through a trait object because the caller cannot guarantee that other has the same concrete type as self.
The Sized problem. Trait objects are dynamically sized types. They can only exist behind a pointer because their size is not known at compile time . If a trait requires Self: Sized, it is saying “this trait can only be implemented by types whose size is known at compile time,” which is the opposite of what a trait object needs. A trait with a where Self: Sized bound on the trait itself cannot be used as dyn Trait .
The receiver problem. A method is dispatchable only if its receiver is a pointer type that can be represented in the vtable: &self, &mut self, self: Box<Self>, self: Rc<Self>, self: Arc<Self>, or self: Pin<P> where P is one of those . A bare self receiver is not allowed because Self would need to be sized and moved through the vtable.
The associated constant problem. A trait with an associated constant like const ID: u32; cannot be dyn-compatible because the vtable cannot store a value that is not a function pointer. Associated types are allowed if the trait object specifies them explicitly, as in dyn Iterator<Item = u8> .
a. The core rules for dyn-compatible traits
The rules can be stated compactly. A trait is dyn-compatible if all of the following hold:
- The trait does not require
Self: Sized. - The trait has no associated constants.
- All supertraits are also dyn-compatible .
- Every method that should be callable through
dyn Traiteither haswhere Self: Sizedor satisfies: no generic type parameters, andSelfappears only in the receiver type .
A method that violates the rules but is marked where Self: Sized is simply not available on trait objects. The trait remains dyn-compatible for its other methods, and the sized method remains available on concrete implementors .
trait Factory {
// Not dispatchable: generic method
fn create<T>(&self) -> T where Self: Sized;
// Dispatchable: no generics, Self only in receiver
fn name(&self) -> &'static str;
}
Factory is now dyn-compatible. dyn Factory has name in its vtable. create exists only on sized implementors. This pattern appears throughout the standard library—Iterator has many where Self: Sized methods so that dyn Iterator remains usable with just next .
b. The Self: Sized escape hatch and its consequences
The where Self: Sized bound is the primary tool for making an otherwise non-dyn-compatible trait usable as a trait object. It tells the compiler: “this method should not be included in the vtable, and it cannot be called through dyn Trait.” The method remains available on any concrete type that implements the trait, including Box<dyn Trait> itself (which is sized) .
The consequence is a split trait surface. Methods without the bound are callable through dyn Trait. Methods with the bound are callable only on concrete types. This is usually what you want: constructors, clone-like methods, and generic utilities are not appropriate for dynamic dispatch, while the core behavioral methods are.
A common mistake is adding where Self: Sized to a method that should be dispatchable. If the method does not have generic parameters and does not return Self, it probably should not have the bound. The bound should be a deliberate exclusion, not a blanket fix for compiler errors.
c. Associated types and the dyn Trait<Assoc = T> syntax
Associated types are allowed in dyn-compatible traits only if the trait object specifies them. dyn Iterator is not a valid type because the compiler does not know the return type of next. dyn Iterator<Item = u8> is valid because the associated type is bound .
trait Source {
type Item;
fn next_item(&mut self) -> Option<Self::Item>;
}
fn take_two(source: &mut dyn Source<Item = u8>) -> Vec<u8> {
[source.next_item().unwrap(), source.next_item().unwrap()]
}
The Item = u8 binding is what makes the vtable coherent. Without it, the compiler would have no way to know the size and layout of the return value. Generic associated types (GATs) are not dyn-compatible because the vtable cannot represent all possible instantiations .
Complete Example Session
The following session builds a parser trait that is partially dyn-compatible, with one method opted out and one dispatchable method.
// ============================================
// PART 1: THE TRAIT
// ============================================
trait Parser {
// Dispatchable: no generics, Self only in receiver
fn parse(&self, input: &str) -> usize;
// Not dispatchable: returns Self
fn clone_box(&self) -> Box<dyn Parser> where Self: Sized + 'static {
Box::new(self.clone())
}
// Not dispatchable: generic
fn map<T>(&self, f: T) where Self: Sized;
}
// ============================================
// PART 2: A CONCRETE IMPLEMENTATION
// ============================================
#[derive(Clone)]
struct WordParser;
impl Parser for WordParser {
fn parse(&self, input: &str) -> usize {
input.split_whitespace().count()
}
fn map<T>(&self, _f: T) {
// implementation detail
}
}
// ============================================
// PART 3: USAGE THROUGH TRAIT OBJECT
// ============================================
fn run(parser: &dyn Parser, input: &str) -> usize {
// Only dispatchable methods are callable here
parser.parse(input)
}
// ============================================
// PART 4: USAGE ON CONCRETE TYPE
// ============================================
fn run_concrete() {
let parser = WordParser;
let count = parser.parse("hello world");
let boxed = parser.clone_box(); // sized method available
println!("{}", count);
}
The Parser trait is dyn-compatible because parse has no generics and does not return Self, while clone_box and map are opted out with where Self: Sized. &dyn Parser can call parse but not clone_box or map. WordParser can call all three. This is the intended pattern: the vtable stays small and coherent, while concrete types retain full functionality.
Quick Reference
Dyn Compatibility Rules
| Rule | Violation | Fix |
|---|---|---|
No Self: Sized supertrait | trait T: Sized | Remove the bound |
| No associated constants | const N: usize | Move to method |
| No generic methods | fn f<T>(&self) | where Self: Sized |
No bare Self return | fn clone(&self) -> Self | Return Box<dyn Trait> or where Self: Sized |
| Valid receiver | &self, &mut self, Box<Self>, Arc<Self> | Use a supported receiver |
| Associated type bound | dyn Iterator | dyn Iterator<Item = T> |
Escape Hatch
| Situation | Fix |
|---|---|
| Method not needed on trait objects | where Self: Sized |
Trait entirely unsuitable for dyn | Split into two traits |
| Constructor methods | where Self: Sized |
Best Practices
✅ Do This:
fn clone_box(&self) -> Box<dyn Parser> where Self: Sized; // ✅ opt out
fn parse(&self, input: &str) -> usize; // ✅ dispatchable
fn take(source: &mut dyn Source<Item = u8>); // ✅ bound assoc type
❌ Don’t Do This:
fn create<T>(&self) -> T; // ❌ no bound
fn clone(&self) -> Self; // ❌ bare Self return
trait Factory: Sized { ... } // ❌ Sized supertrait
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| E0038: not dyn compatible | Generic method or bare Self | where Self: Sized or split trait |
dyn Iterator error | Associated type not bound | dyn Iterator<Item = T> |
clone on trait object fails | Returns Self | Return Box<dyn Trait> |
Method missing on dyn Trait | Opted out with where Self: Sized | Call on concrete type instead |
Real-World Examples
1. Iterator with sized adapters
trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }
// map, filter, etc. have where Self: Sized
2. Clone-like method
fn clone_box(&self) -> Box<dyn Trait> where Self: Sized + 'static;
3. Bound associated type
fn process(iter: &mut dyn Iterator<Item = u8>);
4. Split trait
trait Draw { fn draw(&self); }
trait DrawFactory: Draw { fn create() -> Self where Self: Sized; }
5. Receiver with Arc
fn shared(self: Arc<Self>) -> Arc<dyn Trait>;
Visual
Trait Object Vtable
┌──────────────────────────────────────────────┐
│ &dyn Parser (16 bytes on 64-bit) │
│ ┌─────────────────┬─────────────────────┐ │
│ │ data pointer │ vtable pointer │ │
│ └────────┬────────┴──────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ WordParser │ │ vtable │ │
│ └─────────────────┘ │ parse() │ │
│ │ (no clone_box) │ │
│ └─────────────────┘ │
└──────────────────────────────────────────────┘
Opting Out with where Self: Sized
┌──────────────────────────────────────────────┐
│ TRAIT SURFACE │
│ │
│ dispatchable: parse(&self) │
│ opted out: clone_box() where Self: Sized│
│ opted out: map<T>() where Self: Sized │
│ │
│ dyn Parser → parse only │
│ WordParser → all methods │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Older name | Object safety |
| Modern name | Dyn compatibility |
| Vtable contents | Function pointers only |
| Generic methods | Not dispatchable |
Bare Self return | Not dispatchable |
| Escape hatch | where Self: Sized |
| Associated types | Must be bound on dyn |
| Error code | E0038 |
Key takeaways:
- Dyn compatibility is structural. The vtable cannot contain generic functions or
Self-returning methods, so the compiler rejects traits that require them . where Self: Sizedis the escape hatch. It excludes a method from the vtable while keeping it available on concrete types .- Associated types must be bound.
dyn Iteratoris invalid;dyn Iterator<Item = u8>is valid . - The receiver must be a pointer.
&self,&mut self,Box<Self>, andArc<Self>are dispatchable; bareselfis not . - The rules exist for a reason. They are not arbitrary restrictions—they reflect what a vtable can physically represent.
Remember: Object safety is the compiler’s way of telling you whether a trait’s shape is compatible with runtime dispatch. If a trait has a generic method, a Self return, or an associated constant, it cannot be used as dyn Trait without opting those items out. The fix is almost always where Self: Sized on the offending method, or splitting the trait into a dispatchable core and a sized-only extension. When you see E0038, read it as a structural constraint, not a bug in your code.
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!