Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-059 · Case file with fixtures · Case 31 of 694 · Compiler evidence

E0038: Why a Generic Method Makes a Rust Trait Not Dyn Compatible

Static generic dispatch can monomorphize a method for each type, but a trait object needs one finite vtable shape. Move generic work outside the object-safe boundary or exclude it from dynamic dispatch.

Reviewed
Rust
Rust 1.98.1
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
A trait object calls through one runtime vtable, while a generic method would require an open-ended family of monomorphized entries that the vtable cannot contain.
First discriminating check
Replace the generic method with one method using a concrete erased input or output and check whether the trait then becomes dyn compatible.

A trait can work perfectly in generic code and fail as soon as I write dyn Trait. This one is enough:

trait Repository {
    fn get<T>(&self, key: &str) -> Option<T>;
}

fn use_repository(repository: &dyn Repository) {
    let _: Option<String> = repository.get("name");
}

Rust 1.98.1 reports E0038 and says Repository is not dyn compatible. The failing fixture captures the compiler result.

The word “dyn” changes the dispatch model. A generic bound such as R: Repository keeps the concrete repository type known to the compiler. A trait object erases that concrete type and calls methods through runtime metadata.

A vtable needs a finite set of callable entries

For a trait object, a pointer conceptually carries a data address and a vtable address. The vtable contains function pointers appropriate for the erased concrete type. A non-generic method like this has one clear slot:

fn get_text(&self, key: &str) -> Option<String>;

But the original method describes an open family:

get::<String>
get::<u64>
get::<MyApplicationType>
...

Static dispatch can generate only the instantiations used by the program. A vtable cannot contain one slot for every type which any downstream crate might request. Rust therefore excludes a method with type parameters from dyn dispatch and, unless the method is restricted, the trait itself cannot form a trait object.

The Reference rules for dyn compatibility list this requirement alongside receiver, associated constant, Self, and opaque-return restrictions.

Repair the dynamic boundary, not only the syntax

In the reduced case I make the operation concrete:

trait Repository {
    fn get_text(&self, key: &str) -> Option<String>;
}

Now every implementation contributes one function with one known signature to the vtable. The repaired fixture builds a memory repository, calls it through &dyn Repository, and checks the result on Rust 1.98.1.

This repair is appropriate when the dynamic consumers really ask for text. It would be weak if the repository must return many unrelated domain types, because encoding and decoding policy would become hidden in a vaguely named method.

Keep generic convenience outside the object

A useful design is an object-safe core plus generic extension helpers:

trait Repository {
    fn get_bytes(&self, key: &str) -> Option<Vec<u8>>;
}

trait RepositoryExt: Repository {
    fn decode<T: Decode>(&self, key: &str) -> Option<T> {
        self.get_bytes(key).and_then(|bytes| T::decode(&bytes))
    }
}

impl<R: Repository + ?Sized> RepositoryExt for R {}

The erased repository handles one byte-oriented operation. Generic decoding is statically dispatched at the call site. This separates the part needing runtime substitution from the part needing compile-time type selection.

Excluding one method with Self: Sized

If a generic method is only a convenience for concrete implementors, it can opt out of trait-object calls:

trait Repository {
    fn get_bytes(&self, key: &str) -> Option<Vec<u8>>;

    fn get<T: Decode>(&self, key: &str) -> Option<T>
    where
        Self: Sized,
    {
        self.get_bytes(key).and_then(|bytes| T::decode(&bytes))
    }
}

The trait can then be dyn compatible because callers cannot invoke get<T> through dyn Repository. The method remains available for sized concrete types. This is an honest restriction, not a loophole: the vtable does not need an entry for the excluded method.

Dynamic dispatch may not be required

Sometimes Box<dyn Repository> was introduced only to put different values behind one field. An enum can represent a closed set of repository implementations, and a generic parameter can represent one implementation selected by the caller. Both keep more type information and may support generic methods naturally.

I choose dyn Trait when runtime substitution and an open implementation set matter. I do not treat trait objects as the automatic way to use every trait.

Read E0038 as a dispatch mismatch

The official E0038 explanation covers each rule and the reasons behind them. My debugging sequence is:

  1. Find the exact method or associated item named by the diagnostic.
  2. Draw the vtable slot it would require.
  3. Decide which behavior truly needs dynamic dispatch.
  4. Move generic or Self-dependent behavior to a sized extension when appropriate.
  5. Test through an actual &dyn Trait, not only a generic bound.

The compiler is not saying the trait is badly designed in every context. It says that its current API cannot be represented by the runtime dispatch mechanism requested at this call site. Once I separate those two models, the repair becomes an API decision rather than a fight with E0038.