Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-550 · Case file with fixtures · Case 522 of 694 · Compiler evidence

Self-Based Generic Defaults Must Be Explicit on Rust Trait Objects

A trait object must name one fully determined trait instantiation. A default depending on the erased implementor cannot choose a shared type after erasure.

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

Direct answer

What this Rust failure means

Why it happens
Each implementor substitutes a different Self-based trait parameter, while a trait object needs one shared fully determined callable interface.
First discriminating check
Specify the object parameter explicitly, or redesign heterogeneous outputs with an associated type constraint, enum, generic boundary, or deliberate erasure.

Generic defaults usually remove repetitive syntax. A default written in terms of Self has a special problem at a dyn Trait boundary: the concrete Self is exactly what the trait object erases.

The failing fixture declares Convert<T = Self> and then accepts &dyn Convert. Rust emits E0393 because it cannot choose one T for that trait object.

A trait object needs a specific trait instantiation

dyn Convert<usize> means values behind the object share the Convert<usize> interface. Their concrete implementor types may differ, but the conversion output contract is one known type.

Bare dyn Convert tries to apply the default T = Self. For TextLength, that would mean one trait instantiation; for another implementor, it would mean another. There is no single erased interface shared by those defaults.

The official E0393 explanation describes this as needing a fully defined trait for the object.

The repaired object fixes the output parameter

The repaired fixture implements Convert<usize> and accepts &dyn Convert<usize>. Dynamic dispatch can now use one method signature returning usize regardless of the hidden concrete type.

This is more than compiler syntax. The consumer has stated what it expects from every object in the collection or call boundary.

If different converters produce different output types, one homogeneous trait-object collection may not be the correct representation. An enum, generic function, or erased common output can model that variation explicitly.

Ordinary generic use can apply the default

With a known concrete Self, the compiler can substitute a default based on that type. This makes T = Self useful for traits describing same-type relations or overridable right-hand operands.

Generic parameter defaults are documented in the Reference's generic parameters section. I remember that a default is a substitution rule, not an existential “whatever type fits later.”

At an erased boundary, every parameter that affects method signatures or trait identity needs a shared answer.

Associated types express one output per implementation differently

Sometimes the design wants each implementor to choose one output. An associated type can express that:

trait Convert {
    type Output;
    fn convert(&self) -> Self::Output;
}

A trait object still needs the associated type specified, such as dyn Convert<Output = usize>. Erasure again requires a stable callable interface.

Generic parameters are useful when one concrete type may implement the trait for several T values. Associated types are useful when the implementation selects one canonical output. I choose by cardinality, not to avoid angle brackets.

Trait objects erase implementor identity, not every type fact

The Reference explains trait objects as dynamically sized types combining a base trait, auto traits, and lifetime bounds. Method dispatch hides the concrete implementor, but callers still need method argument and result layouts they can use.

That is why object types often state associated types, generic parameters, and lifetimes. Dynamic does not mean untyped.

Object safety and missing parameters are separate checks

After writing an explicit parameter, a trait may still be unusable as dyn because a method is not dyn-compatible—for example, it returns Self in an unsupported way or has method-level generics.

E0393 addresses the missing Self-based default. I then read any next diagnostic independently. One corrected layer can reveal another real interface constraint.

Public aliases should preserve the chosen parameter

When one object form appears often, I give it a semantic alias such as type LengthConverter = dyn Convert<usize>. The alias reduces repetition without hiding the output decision. I keep that parameter visible in documentation and compile tests, because changing it changes method signatures and which implementations fit. If the object crosses an ownership boundary, I also add the required reference count or box and lifetime separately. Type erasure, storage ownership, and the converter's result type are three different decisions even when one alias eventually packages them together.

My E0393 checklist

  • Does a trait parameter default mention Self?
  • Is the concrete implementor being erased behind dyn?
  • What one parameter value does the consumer actually require?
  • Can I write dyn Trait<Concrete> explicitly?
  • Should an associated type represent one output per implementation?
  • Do heterogeneous outputs need an enum or another erasure layer?
  • Is the trait otherwise dyn-compatible after the parameter is fixed?
  • Does the final object type make its callable contract obvious?

The core principle is that dynamic dispatch erases identity while preserving a concrete interface. I specify every type fact needed to make that interface shared and callable.