🎓 A1Learner
🔍

Rust Object Safety Rules

Object safety is the set of rules a trait must pass to become a trait object. Some traits fail them and cannot become objects.

Safe: plain methods on &self

A method that needs only the receiver can sit in the vtable — the table holds one fixed address for it.

trait Tool {
    fn name(&self) -> String;
    fn use_it(&mut self);
}

Breaker 1: a method returns Self

Through a trait object the real type is erased, so nobody knows what Self is — or how big it is. Refused with error E0038 (checked).

trait Copyable {
    fn duplicate(&self) -> Self; // breaks object safety
}

Breaker 2: a generic method

A vtable holds one address per method. A generic method would need one address per type T — an endless table. Also E0038.

trait Converter {
    fn convert<T>(&self, value: T) -> String; // breaks object safety
}

Breaker 3: Sized as a supertrait

A trait object is unsized by nature — its concrete type is hidden. A trait that demands Self: Sized fights that directly, and is refused as an object.

trait Frozen: Sized {
    fn label(&self) -> String;
}

Breaker 4: an associated constant

Constants live on the type, not on the value — and a trait object has forgotten its type. A trait with an associated const cannot be made into an object either.

trait Named {
    const NAME: &'static str;
    fn greet(&self) -> String;
}

Fix one: excuse a method with where Self: Sized

The method stays on the trait for concrete types, but leaves the vtable. Trait objects simply cannot call it — everything else still works.

fn fresh_copy(&self) -> Self
where
    Self: Sized;

Fix two: return a box, not Self

A Box<dyn Tool> has a known size even when the type inside is erased — so this shape is dispatchable.

fn boxed_copy(&self) -> Box<dyn Tool>;

Example

trait Tool {
    fn name(&self) -> String;

    // Not dispatchable — but allowed, because of the where clause.
    // Concrete types can call it; trait objects skip it.
    fn fresh_copy(&self) -> Self
    where
        Self: Sized;

    // The object-safe way to “copy”: hand back a box instead of Self.
    fn boxed_copy(&self) -> Box<dyn Tool>;
}

#[derive(Clone)]
struct Hammer {
    weight: u32,
}

impl Tool for Hammer {
    fn name(&self) -> String {
        format!("hammer {} kg", self.weight)
    }

    fn fresh_copy(&self) -> Self {
        self.clone()
    }

    fn boxed_copy(&self) -> Box<dyn Tool> {
        Box::new(self.clone())
    }
}

fn main() {
    let tools: Vec<Box<dyn Tool>> = vec![Box::new(Hammer { weight: 2 })];

    // Dispatchable methods work through the trait object.
    println!("{}", tools[0].name());

    // boxed_copy returns a trait object, so it works here too.
    let again = tools[0].boxed_copy();
    println!("{}", again.name());

    // fresh_copy needs a known type — call it on the concrete value.
    let real = Hammer { weight: 5 };
    let copy = real.fresh_copy();
    println!("{}", copy.name());
}

Key points

  • A trait is object safe when every method can sit in a vtable: one fixed address, called through a pointer alone.
  • Object safety is now called dyn compatibility.
  • The reference and error E0038 (“the trait is not dyn compatible”) use the new name.
  • The rules are the same.
  • The vtable test: a method is dispatchable only under one condition.
  • The compiler must store one fixed address for it.
  • It must call it knowing nothing but the pointer.
  • Receivers that work: &self, &mut self, self: Box<Self>, Rc<Self>, Arc<Self>, and Pin versions of them.
  • A bare self taken by value is not dispatchable.
  • Every supertrait must be object safe too: one unsafe supertrait poisons the whole chain
  • Associated types are allowed in trait objects.
  • But they must be pinned in the type.
  • Box<dyn Iterator<Item = u8>> names Item once and for all.
  • These rules bite only when you write the word dyn.
  • Two standard fixes can rescue a trait that breaks a rule.

In this example, Tool carries both fixes at once. fresh_copy returns Self, which no vtable can hold. The where Self: Sized clause excuses it from the vtable. So Box<dyn Tool> still compiles. The concrete Hammer can still call it (the 5 kg line). boxed_copy avoids the problem entirely. It returns Box<dyn Tool>, a type with a known size. So the trait object can call it. The call produces the second 2 kg line. Without either fix, the very first Box<dyn Tool> line would fail with E0038.

Note

Generic methods and Self returns are perfectly fine in generic code (T: Copyable).

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Which method keeps a trait from being used as a trait object?