Skip to content
Languages

11 Rust Traits and Dispatch Interview Questions and Answers

This focused guide turns RecallDeck’s curated Rust Traits and Dispatch material into 11 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

11 min read11 detailed answersReviewed Aug 24, 2026
What to remember

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

01

Static (impl Trait/generics) vs dynamic (dyn Trait) dispatch — what's the difference, and what is monomorphization?

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:

  1. Monomorphizationfn f<T: Trait>(x: T) called with u32 and String compiles to two specialized functions. Hence direct calls, inlining, and binary bloat.
  2. dyn Trait — one shared function hidden behind a vtable. The call is indirect, cross-call inlining is impossible, but the binary is smaller.
  3. HeterogeneityVec<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.

02

How is dyn Trait laid out in memory, and how does a trait object differ from a C++ object with virtual methods?

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:

  1. 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.
  2. 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.
  3. 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.

03

What is object safety? Why can't you have Box<dyn Clone>?

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:

  1. Returning Self by value — the result size is unknown (the Clone case).
  2. Generic methodsfn f<T>(&self) would need infinitely many vtable slots (one per T).
  3. self by value without where Self: Sized — you can't pass an unsized receiver.
  4. 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).

04

Why must dyn Trait always sit behind a pointer (Box, &, Rc), and what does ?Sized mean?

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:

  1. Unsized typedyn Trait and [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).
  2. A pointer pins the sizeBox<dyn Trait> / &dyn Trait are two words wide and know the type via the vtable.
  3. Implicit Sizedfn f<T>(x: T) really means fn f<T: Sized>(x: T). To accept an unsized type you write T: ?Sized and 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.

05

Associated type vs generic trait parameter: Iterator::Item vs From<T> — when do you choose which?

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:

  1. Associated typeIterator with type Item can be implemented for a type only once, so x.next() needs no type annotation.
  2. Generic parameterFrom<T> can be implemented any number of times: i64: From<i32>, i64: From<i16>, and so on.
  3. 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.

06

What is the orphan rule and how does the newtype pattern work around it?

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:

  1. 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.
  2. What's forbidden — implementing a foreign trait for a foreign type: impl Display for Vec<T> outside std won't compile.
  3. 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.

07

How does impl Trait in argument position differ from impl Trait in return position?

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:

  1. Argumentfn f(x: impl Trait) equals fn f<T: Trait>(x: T). The caller picks the type; static dispatch.
  2. Return (RPIT)fn g() -> impl Trait hides the concrete type (e.g. a closure or iterator), but there's exactly one. The compiler infers it from the body.
  3. Branch restriction — if if/else branches return different concrete types, RPIT won't compile; you need Box<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.

08

What bounds does derive(Clone) silently add to generic parameters, and when does that bite?

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:

  1. What the derive does — it generates impl<T: Clone> Clone for Wrapper<T>, mechanically adding T: Clone across all parameters.
  2. Why it bitesArc<T>: Clone doesn't depend on T (the pointer is cloned, not the value), so the extra bound narrows the API for no reason.
  3. The fix — write the impl by hand with the real bounds; then Wrapper<T> clones for any T.
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).

09

How do Deref, AsRef and Borrow differ? Why does HashMap<String, V>::get accept &str?

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:

  1. Deref — about how a type "behaves like" another; enables deref coercion and auto-deref via */..
  2. AsRef<T> — a cheap explicit reference-to-reference conversion; guarantees nothing about hashing or equality.
  3. Borrow<T> — like AsRef but with a contract: the borrowed value hashes and compares identically to the owned one. Hence the signature fn get<Q>(&self, k: &Q) where K: Borrow<Q>.
Trait Guarantee Typical use
Deref "behaves like T" Box<T>, Stringstr
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.

10

Explain the Fn / FnMut / FnOnce closure hierarchy. What does the move keyword actually change?

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:

  1. FnOnce — callable once, because the body takes captures by value. The supertrait of the rest.
  2. FnMut — callable repeatedly while mutating state (&mut to the captures).
  3. Fn — callable repeatedly reading only (& to the captures). The "strongest," usable anywhere FnMut/FnOnce is expected.
  4. move — forces by-value capture (needed for threads and 'static closures), but move || println!("{x}") that just reads x is still Fn.
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.

11

Rust has no class inheritance — how do traits and composition replace classic OOP?

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:

  1. Polymorphism — a shared interface via trait + default methods; statically (generics) or dynamically (dyn).
  2. Blanket implimpl<T: Display> ToString for T gives ToString to every Display type at once — reuse without a hierarchy.
  3. Supertrait ≠ inheritancetrait B: A requires implementing A too, but doesn't inherit its state; it's a constraint, not "is-a."
  4. 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.

Start studying

Keep going

Languages103 min

Top 100 Go Interview Questions and Answers

Prepare with 100 Go interview questions and detailed answers on slices, interfaces, concurrency, runtime, testing, distributed systems, and reliability.

100 detailed answers
RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS