Answer from a language model, not from memorized syntax: name the mechanism, show a small example, and explain the production consequence.
Question set
11 detailed answers
01Static (impl Trait/generics) vs dynamic (dyn Trait) dispatch — what's the difference, and what is monomorphization?
middle
Short answer: With static dispatch the compiler knows the concrete type at compile time and emits a direct call — it can be inlined, with zero overhead. With dynamic dispatch the concrete type is only known at runtime, and the call goes through a vtable behind a pointer. Monomorphization is generating a separate copy of the generic code for each concrete type.
In depth:
- Monomorphization —
fn f<T: Trait>(x: T)called withu32andStringcompiles to two specialized functions. Hence direct calls, inlining, and binary bloat. dyn Trait— one shared function hidden behind a vtable. The call is indirect, cross-call inlining is impossible, but the binary is smaller.- Heterogeneity —
Vec<Box<dyn Trait>>stores objects of different types in one collection; generics can't —Vec<T>holds elements of a single type.
| Criterion | Static (generics) | Dynamic (dyn) |
|---|---|---|
| Resolution | compile time | runtime (vtable) |
| Call cost | zero, inlinable | indirect call |
| Binary size | larger (copies) | smaller |
| Heterogeneous types | no | yes |
⚠️ Common mistake: treating dyn as "always slow." On a hot path the difference is usually within the noise; you pick it for flexibility and less code bloat, not just for performance.
02How is dyn Trait laid out in memory, and how does a trait object differ from a C++ object with virtual methods?
senior
Short answer: &dyn Trait and Box<dyn Trait> are a fat pointer of two words: a pointer to the data and a pointer to the vtable — 16 bytes on x86-64. In C++ the vtable pointer (vptr) lives inside the object itself; Rust keeps the data and the behavior table separate.
In depth:
- Fat pointer —
(data_ptr, vtable_ptr). The data itself doesn't "know" its trait, so you can't rebuild a trait object from an erased pointer like*const ()— there's no way to recover the vtable. - What's in the vtable — pointers to the trait's methods, plus the size, alignment, and drop glue of the concrete type. That's how
Box<dyn Trait>drops and deallocates correctly. - Difference from C++ — there the vptr is embedded in the object, so the object grows by one word while a plain pointer stays thin. In Rust the object stays "clean" and the pointer widens instead.
&dyn Trait (16 bytes on x86-64)
┌───────────┬────────────┐
│ data ptr │ vtable ptr │
└─────┬─────┴──────┬─────┘
│ ▼
│ ┌──────────────────┐
│ │ drop_glue │
│ │ size / align │
│ │ method_0 ... │
▼ └──────────────────┘
[ object data ]
⚠️ Common mistake: thinking Box<dyn Trait> is a single pointer. It's twice as wide as Box<T>, and inside Option/enums that noticeably affects size.
03What is object safety? Why can't you have Box<dyn Clone>?
senior
Short answer: Object safety (now called dyn compatibility in the docs) is the set of rules a trait must satisfy so you can build a dyn Trait from it. Clone breaks them: its method clone(&self) -> Self returns Self by value, but behind dyn the concrete type is erased — the caller knows neither the result's size nor where to put it.
In depth:
For a trait to fit into a vtable, every method must be callable through a pointer without knowing the concrete type. Hence the prohibitions:
- Returning
Selfby value — the result size is unknown (theClonecase). - Generic methods —
fn f<T>(&self)would need infinitely many vtable slots (one perT). selfby value withoutwhere Self: Sized— you can't pass an unsized receiver.- Associated constants and methods with no
self— nothing to anchor in the vtable.
| Violation | Example | Why it breaks the vtable |
|---|---|---|
-> Self |
Clone::clone |
result size unknown |
| generic method | fn f<T>(&self) |
a slot needed per T |
self by-value |
fn consume(self) |
unsized receiver |
no self |
fn new() -> Self |
nothing to dispatch on |
⚠️ Common mistake: just answering "Clone isn't object-safe" with no reason. The interviewer wants at least two concrete violations and the words "returns Self." The workaround is a helper trait CloneBox with clone_box(&self) -> Box<dyn Trait> (the pattern from the dyn-clone crate).
04Why must dyn Trait always sit behind a pointer (Box, &, Rc), and what does ?Sized mean?
middle
Short answer: dyn Trait is a DST (a type with no known size): different implementors have different sizes, so the compiler can't place it directly on the stack. Behind a pointer (&, Box, Rc) the size is known again — it's a fixed-width fat pointer. ?Sized relaxes the implicit Sized bound that every generic parameter gets by default.
In depth:
- Unsized type —
dyn Traitand[T]have no statically known size, so they can't be a variable's value or an argument by value (in a struct — only the last field, which makes the struct itself a DST). - A pointer pins the size —
Box<dyn Trait>/&dyn Traitare two words wide and know the type via the vtable. - Implicit
Sized—fn f<T>(x: T)really meansfn f<T: Sized>(x: T). To accept an unsized type you writeT: ?Sizedand take it by reference.
// accepts both sized types and dyn/slices behind a reference
fn print_it<T: std::fmt::Debug + ?Sized>(x: &T) {
println!("{x:?}");
}
// let x: dyn Trait = ...; // error: size not known
let x: Box<dyn std::fmt::Debug> = Box::new(42); // ok
⚠️ Common mistake: not knowing generics are Sized by default. That's why people are surprised T won't accept str or dyn Trait until they add ?Sized.
05Associated type vs generic trait parameter: Iterator::Item vs From<T> — when do you choose which?
middle
Short answer: Use an associated type when a concrete type has exactly one implementation of the trait (an iterator has one Item). Use a generic parameter when one type implements the trait many times, distinguishing impls by the parameter (From<u32>, From<String>…). A one-to-one relationship → associated type; one-to-many → parameter.
In depth:
- Associated type —
Iteratorwithtype Itemcan be implemented for a type only once, sox.next()needs no type annotation. - Generic parameter —
From<T>can be implemented any number of times:i64: From<i32>,i64: From<i16>, and so on. - Cost of getting it wrong — pick a parameter for a unique relationship and the compiler needs turbofish everywhere (a
collect::<Vec<_>>()-style ambiguity); the API turns noisy.
trait Iterator {
type Item; // exactly one per type
fn next(&mut self) -> Option<Self::Item>;
}
trait From<T> { // many impls per type
fn from(value: T) -> Self;
}
⚠️ Common mistake: making Iterator<Item> generic. Then one type could "iterate" into several different Items, type inference breaks, and you annotate everywhere.
06What is the orphan rule and how does the newtype pattern work around it?
middle
Short answer: The orphan rule lets you implement a trait for a type only if either the trait or the type is defined in your crate. It's part of coherence — the guarantee that no two conflicting impls appear across the ecosystem. The workaround is the newtype: wrap the foreign type in your own struct struct MyVec(Vec<T>) and implement the trait on the wrapper.
In depth:
- Why the rule — without it, crates A and B could each write
impl Display for Vec<T>, and linking both would be ambiguous. Coherence forbids that. - What's forbidden — implementing a foreign trait for a foreign type:
impl Display for Vec<T>outsidestdwon't compile. - Newtype — a local wrapper makes the type "yours," satisfying the rule.
#[repr(transparent)]guarantees the same memory layout — the wrapper is free.
use std::fmt;
#[repr(transparent)]
struct Wrapper(Vec<String>); // the type is now local
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
⚠️ Common mistake: trying impl ExternalTrait for ExternalType and hitting E0117. Mentioning #[repr(transparent)] for a zero-cost wrapper signals a strong candidate.
07How does impl Trait in argument position differ from impl Trait in return position?
middle
Short answer: impl Trait in an argument is sugar for an anonymous generic parameter: the function is monomorphized per concrete argument type. In return position (RPIT) it's one opaque concrete type, the same on every code path; you can't conditionally return two different types.
In depth:
- Argument —
fn f(x: impl Trait)equalsfn f<T: Trait>(x: T). The caller picks the type; static dispatch. - Return (RPIT) —
fn g() -> impl Traithides the concrete type (e.g. a closure or iterator), but there's exactly one. The compiler infers it from the body. - Branch restriction — if
if/elsebranches return different concrete types, RPIT won't compile; you needBox<dyn Trait>or an enum.
fn make() -> impl Iterator<Item = i32> {
if cond() {
(0..10).filter(|x| x % 2 == 0)
} else {
(0..5).map(|x| x * 2) // ERROR: a different concrete type
}
}
// Fix: -> Box<dyn Iterator<Item = i32>> and Box::new(...) in each branch
⚠️ Common mistake: thinking -> impl Trait acts like dyn and allows different types on different paths. It's one static type; for runtime polymorphism you need dyn or an enum.
08What bounds does derive(Clone) silently add to generic parameters, and when does that bite?
middle
Short answer: #[derive(Clone)] puts T: Clone on every generic parameter of the struct, even when it isn't needed. The classic example is struct Wrapper<T>(Arc<T>): Arc<T> clones for any T, but the derive still demands T: Clone. The fix is a hand-written impl with tighter bounds.
In depth:
- What the derive does — it generates
impl<T: Clone> Clone for Wrapper<T>, mechanically addingT: Cloneacross all parameters. - Why it bites —
Arc<T>: Clonedoesn't depend onT(the pointer is cloned, not the value), so the extra bound narrows the API for no reason. - The fix — write the
implby hand with the real bounds; thenWrapper<T>clones for anyT.
use std::sync::Arc;
// Problem: requires T: Clone needlessly
#[derive(Clone)]
struct Wrapper<T>(Arc<T>);
// Fix: clones for ANY T
struct Wrapper2<T>(Arc<T>);
impl<T> Clone for Wrapper2<T> {
fn clone(&self) -> Self { Wrapper2(Arc::clone(&self.0)) }
}
⚠️ Common mistake: assuming #[derive(Clone)] is free for generics. For wrappers over Arc/Rc/PhantomData it forces unnecessary bounds — a known derive-macro wart (the "perfect derive" problem).
09How do Deref, AsRef and Borrow differ? Why does HashMap<String, V>::get accept &str?
senior
Short answer: Deref powers auto-deref and coercions (&String → &str implicitly). AsRef is a cheap explicit conversion .as_ref() with no semantic guarantees. Borrow additionally promises that Hash/Eq/Ord agree between the owned and borrowed forms. HashMap::get relies on exactly this: String: Borrow<str>, and the hash of a &str equals the hash of the corresponding String.
In depth:
Deref— about how a type "behaves like" another; enables deref coercion and auto-deref via*/..AsRef<T>— a cheap explicit reference-to-reference conversion; guarantees nothing about hashing or equality.Borrow<T>— likeAsRefbut with a contract: the borrowed value hashes and compares identically to the owned one. Hence the signaturefn get<Q>(&self, k: &Q) where K: Borrow<Q>.
| Trait | Guarantee | Typical use |
|---|---|---|
Deref |
"behaves like T" |
Box<T>, String→str |
AsRef |
cheap conversion | flexible function arguments |
Borrow |
Hash/Eq/Ord agree |
HashMap/BTreeMap keys |
⚠️ Common mistake: implementing Borrow where Hash/Eq diverge from the owned type. The contract is broken — and HashMap lookups silently fail to find existing keys.
10Explain the Fn / FnMut / FnOnce closure hierarchy. What does the move keyword actually change?
middle
Short answer: Which trait a closure implements is decided by how its body uses the captured variables: reads only → Fn, mutates → FnMut, consumes (moves them out) → FnOnce. The traits nest: Fn: FnMut: FnOnce. move only changes the capture mode — to by-value — not the trait choice.
In depth:
FnOnce— callable once, because the body takes captures by value. The supertrait of the rest.FnMut— callable repeatedly while mutating state (&mutto the captures).Fn— callable repeatedly reading only (&to the captures). The "strongest," usable anywhereFnMut/FnOnceis expected.move— forces by-value capture (needed for threads and'staticclosures), butmove || println!("{x}")that just readsxis stillFn.
| What the body does with captures | Trait |
|---|---|
reads only (&) |
Fn |
mutates (&mut) |
FnMut |
| consumes (by-value) | FnOnce |
⚠️ Common mistake: "move makes the closure FnOnce." Wrong: the trait comes from how captures are used, not from move. A move closure that only reads a Copy value is Fn.
11Rust has no class inheritance — how do traits and composition replace classic OOP?
concept
Short answer: Traits give interface-style polymorphism — with default methods and blanket impls. A supertrait is an obligation to "also implement that trait," not inheritance of fields and behavior. Code reuse in Rust is built on composition: you embed a type in a struct and delegate (including via Deref).
In depth:
- Polymorphism — a shared interface via
trait+ default methods; statically (generics) or dynamically (dyn). - Blanket impl —
impl<T: Display> ToString for TgivesToStringto everyDisplaytype at once — reuse without a hierarchy. - Supertrait ≠ inheritance —
trait B: Arequires implementingAtoo, but doesn't inherit its state; it's a constraint, not "is-a." - Composition — component fields + method delegation replace class chains; "has-a" instead of "is-a."
| OOP concept | Rust counterpart |
|---|---|
| interface | trait |
| abstract method | trait method with no body |
| default method | default trait method |
| behavior inheritance | composition + delegation |
| interface inheritance | supertrait (trait B: A) |
⚠️ Common mistake: calling a supertrait "inheritance." It drags along no fields or implementation — it's just a requirement to implement another trait.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.