A trait can be used two ways — as a trait bound on a generic (fn f<T: Draw>(x: &T)) or as a trait object (fn f(x: &dyn Draw)). The single distinction that generates every tradeoff between them:

Generics keep the concrete type; trait objects erase it.

  • Generics are monomorphized: the compiler stamps out a specialized copy of the code per concrete T, so the concrete type is known at compile time.
  • Trait objects are fat pointers (data ptr + vtable ptr, 16 bytes): the concrete type is erased, only the vtable of method pointers remains. One code instance handles all types.

Everything else is a consequence of keep-vs-erase:

Generics (type kept)&dyn Trait (type erased)
Dispatchstatic, inlinablevtable, 2 derefs, slightly slower
Code size / compile timeone copy per type — biggerone copy total — smaller
Multi-trait boundseasy (T: Debug + Draw)awkward (combinatorial helper traits)
Pass on to Draw-bounded code✓ (concrete type known)can call Draw methods, but ✗ upcast dyn Shapedyn Draw
Heterogeneous Vec✗ (each T is a distinct type)✓ (Vec<&dyn Shape>)
Runtime-loaded types (dlopen)✗ (nothing to monomorphize)✓ (only the vtable is needed)

Advice: prefer generics (faster, best for multi-bounds), reach for trait objects when type erasure is the feature you want — smaller code / faster compiles, heterogeneous collections, or runtime-loaded types.

Note: trait Shape: Draw means Shape also-implements Draw, not Shape is-a Draw. The dyn Shape vtable includes Draw’s methods (so you can call them), but there’s no standalone Draw vtable, so you can’t upcast &dyn Shape to &dyn Draw — no Liskov substitution.

See also

References

Questions

flashcards/rust

What single distinction generates all the generics-vs-trait-objects tradeoffs?::Whether the concrete type is kept (generics/monomorphization) or erased (trait objects). Dispatch, code size, multi-bounds, upcasting, and heterogeneous collections all follow from that.

Generics are monomorphized so the concrete type is kept (static dispatch, code bloat); trait objects are fat pointers so the concrete type is erased (one code copy, vtable indirection).

When should you reach for a trait object instead of generics?::When type erasure is the goal — smaller code / faster compiles, heterogeneous collections (Vec<&dyn Trait>), or runtime-loaded (dlopen) types. Otherwise prefer generics.

Why can’t you upcast &dyn Shape to &dyn Draw even though trait Shape: Draw?::Shape: Draw is also-implements, not is-a. The dyn Shape vtable includes Draw’s methods (callable) but there’s no standalone Draw vtable to convert to — no Liskov substitution.