29 — Advanced Type System

rust

Q1. What is the key difference between an associated type and a generic type parameter on a trait?

rust
trait IteratorLike {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

trait Container<T> {
    fn get(&self, i: usize) -> Option<&T>;
}
  • A type implementing a trait with an associated type may only provide ONE concrete type for it, while a generic trait parameter allows multiple impls for different types on the same type
  • Associated types are resolved at runtime; generic parameters are resolved at compile time
  • Associated types can only be primitive types; generics can be any type
  • There is no functional difference — they are two syntaxes for the same feature
Show Answer

Answer: A — a type may implement a trait with an associated type only once, but can implement a generic-parameter trait many times for different type arguments

Explanation: impl<T> Container<T> for MyType can be written once per distinct T (e.g. Container<i32> for MyType and Container<String> for MyType can coexist), because T is part of the trait signature being implemented. But impl IteratorLike for MyType can only exist once total, with Self::Item fixed to a single concrete type — the associated type is an output determined by the impl, not an input you can vary across multiple impls. Both are fully resolved at compile time (Rust has no runtime generics), and associated types can be any type, not just primitives — this is why real-world Iterator uses an associated Item (one element type per iterator) while traits like From<T> use a generic parameter (many conversions per type).

Q2. What does GAT stand for, and what capability does it add over a plain associated type?

  • Generic Associated Type — an associated type that can itself be generic over a lifetime or type parameter, e.g. type Item<'a>
  • Global Application Trait — a trait implemented automatically for every type in the crate
  • Guaranteed Allocation Type — a marker trait indicating heap-allocated storage
  • Generic Abstract Trait — a trait with no default method implementations
Show Answer

Answer: A — Generic Associated Type: an associated type parameterized by a lifetime or type

Explanation: A plain associated type (type Item;) is a single fixed type per impl. A GAT (type Item<'a>; or type Container<T>;) lets that associated type itself depend on a lifetime or type parameter supplied at the point of use, which is what makes "lending iterators" (an iterator whose next() borrows from self for the duration of the call) expressible — something impossible with the older associated-type system because Item couldn't vary per call. The other three options describe nothing that exists in Rust's type system.

rust

Q3. What is PhantomData<T> primarily used for?

rust
use std::marker::PhantomData;

struct TypedId<T> {
    id: u64,
    _marker: PhantomData<T>,
}
  • Telling the compiler a struct "acts as if" it owns/uses a T for variance, drop-check, and auto-trait purposes, even though no field actually stores a T
  • Allocating a placeholder value of type T lazily on first access
  • Making a struct's fields optional at compile time
  • Forcing T to implement Default so a zero value can be materialized
Show Answer

Answer: A — signals the compiler to treat the struct as if it owns/uses a T, for variance, drop-check, and auto-trait purposes

Explanation: PhantomData<T> occupies zero space at runtime but participates in the type system: it affects variance (does TypedId<&'a str> behave covariantly in 'a?), the drop-checker (does dropping TypedId<T> count as potentially dropping a T, affecting borrow-checking around T's lifetime?), and auto-trait inference (Send/Sync) exactly as if a real T field were present. It's commonly used for phantom-typed IDs, unit-of-measure wrappers, or marking unsafe code's intended ownership. It doesn't allocate anything, doesn't affect field optionality, and doesn't require T: DefaultPhantomData<T> is constructible via PhantomData regardless of what traits T implements.

Q4. Which of the following best describes "object safety" for a trait — i.e. what determines whether dyn Trait is a valid type?

  • A trait is object-safe roughly when none of its methods return Self by value or take generic type parameters, and it has no associated consts (with some further refinements)
  • Every trait in Rust is automatically object-safe; dyn Trait always compiles for any trait
  • A trait is object-safe only if it has exactly one method
  • Object safety is a runtime property checked when the trait object is constructed, not a compile-time one
Show Answer

Answer: A — roughly: no methods returning Self by value, no generic methods, no associated consts (plus a few more refinements)

Explanation: dyn Trait erases the concrete type behind a vtable, so any method whose signature depends on knowing the concrete Self size or identity at compile time (returning Self by value, taking Self by value as a non-receiver parameter, or having generic type parameters that would require monomorphizing per call site) can't be represented in a single vtable entry, making the trait not object-safe as-is (methods can individually opt out via where Self: Sized). This is checked entirely at compile time — attempting Box<dyn Trait> for a non-object-safe trait is a compile error, never a runtime failure. Not every trait qualifies (e.g. Clone is famously not object-safe because fn clone(&self) -> Self returns Self by value), and method count is irrelevant to object safety.

rust

Q5. Why is Clone not object-safe, i.e. why can't you write Box<dyn Clone>?

rust
trait Clone {
    fn clone(&self) -> Self;
}
  • clone(&self) -> Self returns Self by value, and the vtable has no way to know the size of the concrete type to return, since that information is erased
  • Clone requires unsafe internally, which trait objects forbid
  • Clone has too many blanket implementations for the compiler to resolve a vtable
  • Box<dyn Clone> actually does compile; the restriction only applies to &dyn Clone
Show Answer

Answer: A — returning Self by value requires knowing the concrete type's size, which is erased by dyn

Explanation: A dyn Clone trait object has erased its concrete type down to a (data pointer, vtable pointer) pair; the caller of clone() on it has no compile-time knowledge of how large the returned Self is or how to place it on the stack, since different underlying types implementing Clone have different sizes. This makes fn clone(&self) -> Self impossible to call through a vtable in a type-erased way, so the trait fails object safety. Neither unsafe nor blanket impls have anything to do with it, and Box<dyn Clone> fails to compile identically to &dyn Clone — boxing doesn't change the object-safety requirement, since the vtable problem exists regardless of the pointer wrapper.

Q6. Given fn foo<T: Trait>(x: T) versus fn foo(x: impl Trait) versus fn foo(x: Box<dyn Trait>), which statement is correct?

  • The first two are static dispatch (monomorphized per concrete type at compile time); the third is dynamic dispatch through a vtable at runtime
  • All three compile to identical machine code; the syntax differences are purely stylistic
  • impl Trait in argument position uses dynamic dispatch, unlike <T: Trait>
  • Box<dyn Trait> is resolved at compile time via monomorphization just like generics
Show Answer

Answer: A — the first two are static dispatch via monomorphization; the third is dynamic dispatch via a vtable

Explanation: Performance: fn foo<T: Trait>(x: T) and fn foo(x: impl Trait) are exactly equivalent sugar for each other — both cause the compiler to generate a separate specialized copy of foo for every concrete type used at call sites (monomorphization), enabling inlining and eliminating indirect calls, at the cost of larger binary size ("code bloat") if used with many types. Box<dyn Trait> instead compiles foo once, taking a fat pointer, and dispatches each method call through the vtable at runtime — smaller binary, one indirect call per invocation, no inlining across the call boundary. They are not machine-code-identical, and impl Trait in argument position is definitively static, not dynamic, dispatch.

Q7. What does it mean for a generic type to be "covariant" in a lifetime parameter, using &'a T as the canonical example?

  • If 'long: 'short (i.e. 'long outlives 'short), then &'long T can be used wherever &'short T is expected — the subtyping relationship on the lifetime carries over to the reference type
  • Covariance means the compiler automatically converts &'a T into &'a mut T when needed
  • Covariance means the type can be mutated through an immutable reference
  • Covariance only applies to trait objects, never to plain reference types
Show Answer

Answer: A — a longer-lived reference can be used where a shorter-lived one is expected, mirroring the lifetime subtyping relationship

Explanation: &'a T is covariant in 'a: because a longer lifetime is considered a "subtype" of a shorter one (anything valid for longer is trivially valid for a shorter window), &'long T can be passed anywhere &'short T is needed automatically — this is what lets you pass a &'static str to a function expecting &'a str for any 'a. It has nothing to do with converting immutable to mutable references (that's an entirely separate, forbidden operation) or mutating through &T (that's interior mutability via Cell/RefCell, unrelated to variance). Variance applies broadly to any generic type over lifetimes or type parameters, not only trait objects — Vec<T> is covariant in T, for instance.

rust

Q8. Cell<T> and RefCell<T> are invariant in T, while &T and Vec<T> are covariant in T. Why does RefCell<T> need to be invariant even though &T is covariant?

rust
struct RefCell<T> {
    value: UnsafeCell<T>,
}
  • Because RefCell allows mutation through a shared reference (interior mutability); if it were covariant, you could smuggle a shorter-lived value in through a longer-lived alias and later read it back out as the longer lifetime, unsoundly extending it
  • Invariance is just a conservative default the compiler applies to any struct with more than one field
  • RefCell<T> is actually covariant; only Cell<T> is invariant
  • Invariance only matters for unsafe code; RefCell doesn't use unsafe internally so this is moot
Show Answer

Answer: A — covariance plus interior mutability would let a shorter-lived value be written in and later smuggled out under a longer lifetime, unsoundly extending it

Explanation: Safety: If RefCell<&'short U> could coerce to RefCell<&'long U> (covariance), code holding the RefCell<&'long U> handle could .replace() in a value borrowed for only 'short, then later .borrow() it back out believing it's valid for 'long — a lifetime-extension unsoundness bug. Because RefCell exposes mutation through &self via UnsafeCell, the type system must be conservative and treat T as invariant (RefCell<T> is neither a subtype nor supertype of RefCell<U> even if T/U are related), closing that hole. This isn't a generic "more than one field" rule — variance is derived structurally from how T is actually used inside the type, and RefCell genuinely does rely on unsafe (UnsafeCell) internally precisely because interior mutability requires it; that's exactly why its variance can't be left permissive.

rust

Q9. What happens if you attempt to define a GAT-style associated type where the type parameter needs a where Self: 'a bound but you omit it?

rust
trait LendingIterator {
    type Item<'a> where Self: 'a;
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
  • Omitting a required where Self: 'a clause on a GAT typically produces a compile error demanding the bound, because without it the compiler cannot prove the associated type's lifetime is compatible with borrows of Self
  • The bound is optional stylistic sugar; omitting it silently defaults to 'static
  • Omitting it causes a runtime panic the first time the associated type is instantiated
  • It compiles identically either way; where Self: 'a has no semantic effect on GATs
Show Answer

Answer: A — omitting a required where Self: 'a clause is a compile error demanding the bound

Explanation: GATs that borrow from Self (like a lending iterator's Item<'a>) generally need a where Self: 'a clause so the compiler can verify, for every concrete lifetime 'a the associated type gets instantiated with, that Self itself lives at least that long — without it, downstream code manipulating Self::Item<'a> couldn't safely assume this relationship and the compiler rejects the trait definition (or usages of it) with a lifetime-bound error. It's not sugar and not optional when the implementation actually needs it, there's no 'static default, and this is caught entirely at compile time — GATs, like all Rust generics, have no runtime instantiation step to panic during.

Q10. A trait method has this signature: fn process<T: Send>(&self, val: T);. Can this trait still be object-safe overall (assuming this is its only method)?

  • No — a generic method (one with its own type parameters beyond Self) makes the trait not object-safe, because the vtable would need a separate entry per possible T, which is unbounded
  • Yes — Send bounds have no effect on object safety, only lifetime bounds do
  • Yes — as long as T: Send and not T: 'static, generic methods are exempted from the object-safety rule
  • No — but only because &self should be &mut self for the method to be considered
Show Answer

Answer: A — a generic method makes the trait not object-safe, since the vtable can't have unboundedly many entries

Explanation: A vtable is a fixed, finite table of function pointers built once per concrete implementing type; a generic method like process<T> would need a distinct function pointer per possible T used at any call site anywhere in the program, which is open-ended and unknowable at the point the vtable is built. This makes any trait with a generic method (regardless of what bound, Send or otherwise, is on that generic parameter) non-object-safe for that method — though the trait can still be used as impl Trait or with static dispatch, or the method can be excluded from the vtable via where Self: Sized on just that method. The receiver being &self vs &mut self is unrelated to this particular restriction.

rust

Q11. What is the compile-time behavior of this code, given that Iterator has one associated type Item and dyn Iterator (no type argument) is written without specifying it?

rust
fn make_iter() -> Box<dyn Iterator> {
    Box::new(vec![1, 2, 3].into_iter())
}
  • Compile error — a trait object over a trait with an associated type must specify that associated type, e.g. dyn Iterator<Item = i32>
  • Compiles fine; the compiler infers Item from the Box::new argument automatically
  • Compiles fine; Item defaults to () when omitted from a trait object
  • Compiles fine; associated types are erased entirely for trait objects and don't need specifying
Show Answer

Answer: A — compile error; trait objects must specify associated types explicitly, e.g. dyn Iterator<Item = i32>

Explanation: Debug: Unlike a generic type parameter, an associated type is part of the trait's contract that must be pinned down to form a concrete, well-defined vtable — dyn Iterator alone is ambiguous about what next() returns, so Rust requires dyn Iterator<Item = i32> (or whatever concrete item type) to be written explicitly. This differs from ordinary type inference elsewhere in the language; there's no defaulting to (), no erasure of associated types (the vtable's next method needs a known return layout), and no automatic inference from the constructor argument in the return-type position — the function signature itself must be unambiguous independent of the body.

rust

Q12. What is the effect of #[non_exhaustive] combined with a private field pattern versus using PhantomData purely for sealing a trait so external crates cannot implement it — which correctly implements the "sealed trait" pattern?

rust
mod sealed {
    pub trait Sealed {}
}

pub trait MyTrait: sealed::Sealed {
    fn method(&self);
}
  • Making the public trait require a supertrait defined in a private module that external crates cannot name or implement, so they cannot satisfy the supertrait bound and thus cannot implement the public trait
  • Adding #[non_exhaustive] to the trait definition itself prevents external implementations
  • Using PhantomData as an associated type prevents external crates from implementing the trait
  • There is no way to prevent external crates from implementing a public trait; visibility only affects structs and enums
Show Answer

Answer: A — require a supertrait from a private module that outside crates can't name or implement

Explanation: Idiom: The "sealed trait" pattern exploits the fact that implementing MyTrait requires also implementing sealed::Sealed, but sealed is a private module — external crates can see and use MyTrait (it's pub) but cannot name sealed::Sealed to provide an impl of it, so they're structurally barred from ever implementing MyTrait, while code within the defining crate can implement both freely. #[non_exhaustive] only affects exhaustive matching/construction of structs, enums, and variants — it has no effect on trait implementability. PhantomData is a marker for variance/drop-check, unrelated to sealing. This pattern is exactly why sealing traits is possible, contrary to the last option.

rust

Q13. What's wrong with this attempt to implement a trait generically for a type parameter, assuming Wrapper<T> is a local struct and Display is from std::fmt?

rust
struct Wrapper<T>(T);

impl<T> std::fmt::Display for T {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "generic")
    }
}
  • This violates the orphan rule: you cannot implement a foreign trait (Display, from std) for a fully generic, unconstrained type T that isn't local to this crate — it must be a local type or the trait must be local
  • This is valid but only compiles in edition 2021 and later
  • The error is that Wrapper<T> should have been used instead of T, but otherwise the pattern is fine
  • This is legal Rust; the orphan rule only restricts impl blocks for concrete foreign types, not generic ones
Show Answer

Answer: A — this violates the orphan rule; both the trait and the blanket-implemented type must have some local connection, and a bare unconstrained T has none

Explanation: Safety: The orphan rule requires that for impl Trait for Type, either Trait or Type be local to the current crate (with generic parameters needing to appear "covered" by a local type when the trait is foreign). Here Display is foreign (from std) and T is a fully generic, uncovered type parameter — not Wrapper<T>, which would be local and legal (impl<T: Display> Display for Wrapper<T> compiles fine). Allowing a blanket foreign-trait-for-any-T impl would let any crate globally claim Display for every type in the ecosystem, causing unresolvable conflicts if two crates both tried it — exactly the coherence violation the orphan rule exists to prevent. This is not edition-gated; the rule is a fundamental, long-standing part of trait coherence.

rust

Q14. What does Self: Sized mean when added as a bound to an individual trait method, and why would you add it?

rust
trait Shape {
    fn area(&self) -> f64;
    fn scaled(self, factor: f64) -> Self where Self: Sized;
}
  • It opts that specific method out of being included in the trait's vtable, allowing the rest of the trait to remain object-safe even though this one method (which takes/returns Self by value) could not itself be called through dyn Shape
  • It forces every implementor of the trait to be a fixed, statically known size at compile time, which all Rust types already are by default anyway so the bound is meaningless
  • It's required on every trait method as boilerplate and has no functional effect
  • It disables monomorphization for that method, forcing dynamic dispatch even when called via a generic
Show Answer

Answer: A — it excludes that method from the vtable, letting the rest of the trait stay object-safe despite this method being incompatible with dyn

Explanation: Idiom: Methods that take or return Self by value (like scaled, which consumes and returns Self) are individually incompatible with dynamic dispatch for the same reason Clone::clone is — the vtable can't represent an unsized-context by-value Self. Adding where Self: Sized to just that method tells the compiler "this method is unavailable when called through dyn Shape," which excludes it from the vtable while leaving the rest of the trait (like area(&self)) usable through dyn Shape — a common technique to make otherwise-inconvenient traits partially object-safe. It's not boilerplate (most methods don't need it), it doesn't relate to monomorphization/dispatch selection for other calls, and while most types genuinely are Sized by default, trait objects (dyn Trait) and slices ([T]) are the important unsized exceptions this bound is specifically excluding.

Q15. When designing a trait, when should you prefer an associated type over a generic type parameter as a matter of API design idiom?

  • When there is logically only one sensible output type per implementation (e.g. one Item type per iterator, one Error type per parser) — use an associated type; when a type can meaningfully implement the trait multiple times for different type arguments, use a generic parameter
  • Always prefer generic type parameters; associated types are a legacy feature kept only for backward compatibility
  • Associated types should be used whenever performance matters, since they avoid monomorphization
  • It's purely a stylistic choice with no design implications either way
Show Answer

Answer: A — use an associated type when there's one canonical output per impl; use a generic when a type can implement the trait multiple ways

Explanation: Idiom: This is the standard Rust API-design heuristic: Iterator::Item, Add::Output, and Deref::Target are associated types because a given type has exactly one sensible answer (a Vec<i32>'s iterator only ever yields i32), which also gives callers cleaner type inference (no need to annotate Item at every call site). From<T>/Into<T> are generic-parameterized because a single type genuinely can convert from many different source types (String: From<&str>, String: From<char>, etc.), which associated types couldn't express since each impl would collide. Neither choice affects monomorphization or dispatch strategy by itself — that's a function of dyn vs static generics, orthogonal to associated-type-vs-generic-parameter. This is a real, consequential API design decision, not a stylistic wash.

Q16. A crate defines trait Repository<T> { fn save(&self, item: T); } and separately has three concrete repository structs. A reviewer suggests switching to trait Repository { type Item; fn save(&self, item: Self::Item); } instead. When is this refactor the better idiomatic choice?

  • When each concrete repository type is only ever meant to store one specific item type — the associated-type version prevents accidentally implementing Repository<Foo> and Repository<Bar> on the same struct and simplifies generic code that's writing against impl Repository without specifying T everywhere
  • Never — generic parameters are strictly more powerful and associated types should be avoided whenever a generic would also work
  • Only when T is a primitive type like i32 or bool
  • Only if Repository needs to be object-safe, since generic-parameter traits are always automatically object-safe
Show Answer

Answer: A — when each repository is meant to store exactly one item type, associated types prevent accidental multi-impl and simplify generic call sites

Explanation: Idiom: If UserRepository should only ever save Users, the generic version Repository<T> technically permits also implementing Repository<OtherType> on the same struct by accident (nothing stops it), while Repository { type Item; } makes "one item type per repository" a structural guarantee. It also means code generic over fn process<R: Repository>(repo: R) doesn't need an extra T parameter threaded through — R::Item is derivable. This is a real trade-off, not a strict-dominance situation — generics remain correct when multiple simultaneous implementations genuinely make sense (as in Q1's Container<T>). Object safety is unrelated: a generic-parameter trait like Repository<T> is not automatically object-safe either — dyn Repository<T> still needs a concrete T picked, same as an associated type needing to be pinned down; generic methods within an impl are what break object safety, not the presence of a type parameter on the trait itself.

Q17. In idiomatic Rust, when should a public API function accept impl Trait in argument position versus &dyn Trait?

  • Prefer impl Trait (or a generic bound) by default for performance-sensitive or small-surface-area APIs since it enables inlining and avoids vtable indirection; reach for &dyn Trait when you need to store heterogeneous trait objects in a collection, reduce compile times/binary size from excessive monomorphization, or avoid generic code bloat across many call sites
  • Always use &dyn Trait; impl Trait in argument position is deprecated
  • They are interchangeable in every context including trait method signatures used as trait objects
  • Prefer impl Trait only for return types, never for arguments — using it for arguments is a syntax error
Show Answer

Answer: A — default to impl Trait/generics for performance; use &dyn Trait for heterogeneous collections or to curb monomorphization bloat

Explanation: Performance/Idiom: This mirrors the static-vs-dynamic dispatch trade-off from Q6: impl Trait arguments get monomorphized per call site, which is usually the right default for hot paths since it allows inlining, but if a function is called with many different concrete types across a large codebase, or if you need a Vec<Box<dyn Trait>> of mixed concrete types, dyn Trait avoids both the code-size explosion and enables true runtime heterogeneity that generics fundamentally cannot express (a Vec<T> can only hold one T). impl Trait in argument position is valid, current, non-deprecated syntax and works in both positions (though the two behave differently in the two positions regarding caller vs. callee choosing the type) — this option is fabricated. And impl Trait cannot be used directly in a trait method signature that also needs to be object-safe/used as dyn, since that would itself introduce a hidden generic parameter, colliding with object-safety rules from Q10.

rust

Q18. What is the idiomatic reason to reach for a GAT instead of just returning owned data (cloning) from a trait method that would otherwise need to borrow from &mut self per call?

rust
trait WindowIterator {
    type Window<'a> where Self: 'a;
    fn next_window(&mut self) -> Option<Self::Window<'_>>;
}
  • To avoid the allocation/clone cost of materializing owned data on every call when the underlying data can instead be borrowed directly, which matters in hot loops over large buffers
  • GATs are purely a compile-time convenience with no runtime performance implication either way
  • Cloning is always faster than borrowing in Rust, so GATs are used for API ergonomics only, never performance
  • GATs eliminate the need for lifetimes entirely in the method signature
Show Answer

Answer: A — to avoid allocation/clone overhead by borrowing directly instead of materializing owned copies each call

Explanation: Performance: Before GATs, a trait method wanting to return "a view into self" per call had no way to express a per-call lifetime in an associated type, so implementors were forced to either return owned/cloned data (extra allocation and copying on every iteration) or use awkward external-iteration workarounds. GATs let Window<'a> borrow from self for exactly the call's lifetime, which is why "lending iterators" over large in-memory buffers (audio samples, matrix rows, parser tokens) benefit — no per-item allocation. It's very much a runtime-performance-relevant feature, not merely compile-time sugar, cloning is not "always faster" (it's essentially always slower or equal, since it does strictly more work than a borrow), and lifetimes remain very much present in GAT signatures (Self::Window<'_>) — GATs add lifetime parameters to associated types, they don't remove lifetimes from the picture.

Q19. Why does the standard library NOT define Iterator::next(&mut self) -> Option<Self::Item<'_>> (i.e. why is the standard Iterator::Item not a GAT), even though a "lending iterator" would seem more general?

  • Making Item a GAT would break an enormous amount of existing code that relies on being able to hold multiple yielded items simultaneously (e.g. collecting into a Vec), since a lending iterator's items are tied to the borrow of self and can't outlive the next call
  • The standard library simply hasn't gotten around to it yet and it's a planned future breaking change
  • GATs didn't exist when Iterator was designed, and Rust never changes existing trait definitions for any reason
  • Iterator::Item actually is a GAT already; this question's premise is false
Show Answer

Answer: A — a GAT-based Item would tie each yielded item to the borrow of self, breaking the enormous amount of code that holds multiple items at once (e.g. collect())

Explanation: Safety/Idiom: Iterator's Item is deliberately a plain (non-GAT) associated type precisely so that iter.next() returns something with no borrow-tie to self, letting you do v.iter().cloned().collect::<Vec<_>>() or hold many yielded items alive at once — a lending-iterator Item<'a> tied to &'a mut self would forbid exactly this pattern, since each item would have to be dropped or copied before calling next() again. This is a genuine, permanent API-design decision (there's ongoing separate work on a distinct LendingIterator-style trait, but it is not, and won't be, a change to Iterator itself) — not a stopgap due to GATs being unavailable at design time (GATs postdate Iterator by years, correct, but that's not why the design stands: it stands because it's the right design for the vast majority of iteration use cases). Iterator::Item in std remains a plain associated type today.

rust

Q20. A team is designing a plugin trait meant to be stored as Vec<Box<dyn Plugin>> and called uniformly. One team member proposes adding fn configure<C: Config>(&mut self, cfg: C) to the trait. What's the best-practice critique?

rust
trait Plugin {
    fn run(&mut self);
    fn configure<C: Config>(&mut self, cfg: C);
}
  • The generic method makes Plugin not object-safe, which directly conflicts with the stated goal of storing plugins as Box<dyn Plugin>; it should instead take a concrete or enum-based config type, or be moved to a separate non-object-safe extension trait
  • There's no issue; generic methods work fine on trait objects as long as C: Config is a marker trait with no methods
  • The fix is to add where Self: Sized to run instead of touching configure
  • Box<dyn Plugin> would still compile, but calling .configure() through it would panic at runtime instead of failing to compile
Show Answer

Answer: A — a generic method breaks object safety, conflicting with the Box<dyn Plugin> requirement; use a concrete/enum config type or split it into a separate trait

Explanation: Idiom/Debug: As established in Q10, any generic method (regardless of what the bound C: Config requires) makes a trait non-object-safe because the vtable can't have unbounded entries — this isn't about whether Config has methods, it's structural. Given the explicit design goal of Vec<Box<dyn Plugin>>, the fix must remove the genericity from the object-safe surface: accept a concrete type (&dyn Config or a specific PluginConfig struct/enum) instead of C: Config, or split configure off into a separate, non-object-safe trait that's used only where static dispatch is acceptable. Adding where Self: Sized to run (an unrelated, already-object-safe method) does nothing to fix configure. And critically, this is a compile-time rejection — Box<dyn Plugin> with a generic method present simply fails to compile at the trait-object-construction or vtable-formation point; there is no runtime panic path here, since Rust never allows constructing a value of a type it can't statically verify.