Advanced Traits

Object Safety and Dyn Compatibility

A trait is dyn-compatible (historically called object-safe) if it can be used as a trait object type dyn Trait (such as &dyn Trait, Box<dyn Trait>, or Arc<dyn Trait>). Starting in recent rustc versions (1.82+ / Rust 2024), compiler diagnostics use the term dyn-compatible (E0038: the trait ... is not dyn compatible) because it directly names the dyn keyword.

Why dyn Trait Requires Vtable-Compatible Items

When a concrete type T is erased behind &dyn Trait, the compiler constructs a static virtual method table (vtable) containing function pointers for each trait method. Because callers only have a two-word fat pointer (data_ptr, vtable_ptr) and do not know the concrete type T or its size at compile time, every vtable method must have a single fixed machine-code signature and known stack argument sizes.

What a `dyn Trait` Vtable Must Know at Compile Time:
+---------------------------------------------------+
| drop_in_place: unsafe fn(*mut ())                 |
| size:          usize                              |
| align:         usize                              |
| method_1:      fn(*const (), arg1) -> Ret         | <-- Fixed signature!
| method_2:      fn(*mut (), arg1, arg2) -> Ret     |     (Cannot be generic `<U>`,
+---------------------------------------------------+      cannot take/return `Self` by value!)

Complete Rules for Dyn Compatibility

A trait Trait is dyn-compatible if and only if both its supertraits and its own items satisfy these rules:

Trait Item / Property Dyn-Compatible? Reason & How to Make Dyn-Compatible
Trait: Sized supertrait bound No dyn Trait is unsized (!Sized)
Method taking &self, &mut self, Box<Self>, Rc<Self>, Arc<Self>, or Pin<P> Yes Receiver is a pointer with known size (DispatchFromDyn)
Method taking self by value (fn consume(self)) No Caller does not know how many stack bytes self occupies; add where Self: Sized or take self: Box<Self>
Method returning Self by value (fn clone(&self) -> Self) No Caller cannot allocate stack space for unknown return size; add where Self: Sized
Method with Self in another argument (fn eq(&self, other: &Self)) No Two &dyn Trait values could have different underlying concrete types!
Generic method (fn serialize<W: Write>(&self, w: W)) No A finite vtable cannot hold infinitely many monomorphized <W> function pointers; add where Self: Sized or accept &mut dyn Write
Associated function without self receiver (fn new() -> Self) No No instance exists from which to read a vtable pointer; add where Self: Sized
Associated constant (const ID: u32) No Cannot be accessed on dyn Trait; move to a separate trait or method fn id(&self) -> u32
Plain associated type (type Output;) Yes (if specified) Must be specified in the trait object type: dyn Trait<Output = u32>
Generic Associated Type (type Item<'a>;) No (if used in vtable methods) Unbounded family of types cannot be fixed in dyn Trait

Exempting Non-Dispatched Methods with where Self: Sized

Adding where Self: Sized to a constructor (fn new() -> Self where Self: Sized), a by-value method, or a generic helper method excludes that specific method from the dyn Trait vtable. Concrete types can still call the method normally, while dyn Trait remains completely valid!

trait Plugin {
    // Excluded from the `dyn Plugin` vtable via `where Self: Sized`:
    fn from_name(name: &str) -> Self
    where
        Self: Sized;

    // Included in the vtable: takes `self: Box<Self>` so a boxed trait object
    // can consume itself by value!
    fn execute_once(self: Box<Self>) -> String;

    // Included in the vtable: standard `&self` receiver:
    fn label(&self) -> &str;
}

struct LoggerPlugin {
    prefix: String,
}

impl Plugin for LoggerPlugin {
    fn from_name(name: &str) -> Self {
        Self {
            prefix: format!("[{name}]"),
        }
    }

    fn execute_once(self: Box<Self>) -> String {
        format!("{} executed and consumed", self.prefix)
    }

    fn label(&self) -> &str {
        &self.prefix
    }
}

fn main() {
    // Call `from_name` on the concrete type:
    let concrete = LoggerPlugin::from_name("audit");

    // Erase into `Box<dyn Plugin>`:
    let boxed_plugin: Box<dyn Plugin> = Box::new(concrete);
    println!("plugin label:  {}", boxed_plugin.label());
    println!("plugin result: {}", boxed_plugin.execute_once());
}
plugin label:  [audit]
plugin result: [audit] executed and consumed