TRT
Sign in
easyCoreconcept

Contrast static dispatch (`impl Trait` / `<T: Trait>`) and dynamic dispatch (`&dyn Trait` / `Box<dyn Trait>`) in Rust—how is a Rust vtable laid out, what are.

Explain monomorphization + LLVM inlining, the 16-byte `(data_ptr, vtable_ptr)` fat pointer, `where Self: Sized` escape hatches on object-safe traits, and the inner non-generic function idiom.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

Rust gives you explicit control over the two ways machines execute polymorphic code: (1) Static Dispatch (fn run<T: Trait>(x: &T) or x: &impl Trait) uses Monomorphization: at compile time, the compiler stamps out a separate, specialized copy of run for each concrete type (run_FileHandler, run_MemHandler). Each call is a direct static jump that LLVM can inline completely (0 ns dispatch overhead!), at the cost of larger binary .text size. (2) Dynamic Dispatch (&dyn Trait or Box<dyn Trait>) erases the concrete type behind a 16-byte Fat Pointer: Word 0 (data_ptr) points to the struct instance, and Word 1 (vtable_ptr) points to a static read-only Virtual Method Table (vtable) containing (drop_in_place, size, align, method_1_ptr, method_2_ptr, ...)! Note how this differs from C++: in C++, every polymorphic object embeds a vptr header inside the object itself; in Rust, the struct has 0 bytes of vptr overhead, and the vtable_ptr is carried alongside data_ptr in the 16-byte &dyn Trait reference only when dynamic dispatch is requested!

A concrete struct in Rust never carries a hidden vtable pointer (size_of::<MyStruct>() is unchanged when implementing traits); converting &MyStruct (8 bytes) into &dyn Trait produces a 16-byte fat pointer (data_ptr, vtable_ptr) if and only if Trait is dyn-compatible (object-safe). For a trait to have a fixed-layout vtable array of function pointers at compile time, none of its vtable-dispatched methods can be generic (fn fooT(&self) would require infinitely many vtable slots for every possible T!) or return Self by value (whose stack size is unknown to the caller).

Monomorphized generics (<T: Trait>) enable inlining and loop vectorization (0 dispatch overhead), whereas dyn Trait costs 1 indirect function call through vtable_ptr (~2–4 ns, prevents inlining) while keeping binary code size compact and allowing heterogeneous collections (Vec<Box<dyn Trait>>). In production systems (Rust standard library std::fs::File::open<P: AsRef<Path>> (inner non-generic _open(p.as_ref())), tokio boxed futures Pin<Box<dyn Future>>, and Axum / Tower BoxCloneService middleware stacks.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how static dispatch (Monomorphization) vs dynamic dispatch (dyn Trait vtables) operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Static <T: Trait> Monomorphization, Dynamic &dyn Trait / Box<dyn Trait>) with concrete numbers.
  • Identifies real-world failure modes (Using Box<dyn Trait> inside a 100M-iteration numeric or iterator inner loop) and explains how production systems mitigate them.

Common mistakes

  • Treating static dispatch (Monomorphization) vs dynamic dispatch (dyn Trait vtables) as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Writing a 300-line generic function pub fn load<P: AsRef<Path>>(p: P) without delegating to a non-generic inner function).
  • Forgetting boundary conditions and error recovery (Returning -> Box<dyn Iterator<Item = i32>> when only one concrete iterator chain is returned).

This answer walks

Practice follow-ups

  1. 01If a trait has 5 normal methods and 1 helper method that returns `Self` or uses generics, how can you keep the trait dyn-compatible (`Box<dyn MyTrait>`) by adding `where Self: Sized` to that 1 method?
  2. 02When you have a closed set of 4 known backend types in your own crate, why is an `enum Backend { S3(S3Client), Local(DiskClient), ... }` often faster and more ergonomic than `Box<dyn BackendTrait>`?

Not started

Sign in to save your learning progress.

Sign in to save