| |

Rust 66 🦀 Object Safety Rules for Trait Objects

⏱️ Estimated read: 10 min | |

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 Trait either has where Self: Sized or satisfies: no generic type parameters, and Self appears 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

RuleViolationFix
No Self: Sized supertraittrait T: SizedRemove the bound
No associated constantsconst N: usizeMove to method
No generic methodsfn f<T>(&self)where Self: Sized
No bare Self returnfn clone(&self) -> SelfReturn Box<dyn Trait> or where Self: Sized
Valid receiver&self, &mut self, Box<Self>, Arc<Self>Use a supported receiver
Associated type bounddyn Iteratordyn Iterator<Item = T>

Escape Hatch

SituationFix
Method not needed on trait objectswhere Self: Sized
Trait entirely unsuitable for dynSplit into two traits
Constructor methodswhere 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

PitfallWhy It HappensFix
E0038: not dyn compatibleGeneric method or bare Selfwhere Self: Sized or split trait
dyn Iterator errorAssociated type not bounddyn Iterator<Item = T>
clone on trait object failsReturns SelfReturn Box<dyn Trait>
Method missing on dyn TraitOpted out with where Self: SizedCall 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

ItemValue
Older nameObject safety
Modern nameDyn compatibility
Vtable contentsFunction pointers only
Generic methodsNot dispatchable
Bare Self returnNot dispatchable
Escape hatchwhere Self: Sized
Associated typesMust be bound on dyn
Error codeE0038

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: Sized is the escape hatch. It excludes a method from the vtable while keeping it available on concrete types .
  • Associated types must be bound. dyn Iterator is invalid; dyn Iterator<Item = u8> is valid .
  • The receiver must be a pointer. &self, &mut self, Box<Self>, and Arc<Self> are dispatchable; bare self is 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.


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!