yoklainterview sim

Backend Rs Memory Smart Pointers Unsafe Interview Questions

75 verified Backend Rs Memory Smart Pointers Unsafe interview questions — solve with answers, learn from explanations, test yourself in a real simulation.

Try the real simulation →

Sample questions

Rs Memory Smart Pointers UnsafeDifficulty 1
enum List {
    Cons(i32, List),
    Nil,
}
fn main() {
    let _l = List::Nil;
}

What does rustc (edition 2021) do with this program?
  • aIt compiles; List::Nil occupies 0 bytes so no heap allocation is needed for the empty list
  • bE0072: List has infinite size; the variant needs an indirection such as Box<List>
  • cIt compiles, but constructing any Cons node at runtime overflows the stack and panics
  • dIt fails with E0382 since the inner List is moved into Cons while still being defined
Explanation:An enum must have a known, finite size at compile time. Cons(i32, List) stores a List inline, so the size of List would depend on itself. The compiler rejects this with E0072 (measured on rustc 1.98). Putting the recursive part behind a pointer (Cons(i32, Box<List>)) gives the variant a fixed 8-byte field, and the whole enum then measures 16 bytes on a 64-bit target. There is no runtime stage here at all: the program never gets past type checking.
Rs Memory Smart Pointers UnsafeDifficulty 2
use std::mem::size_of;
fn main() {
    println!("{} {}", size_of::<Option<Box<i32>>>(), size_of::<Option<i32>>());
}

On a 64-bit target, what does this print?
  • a16 8: the Option always adds an 8-byte tag in front of the Box pointer, while i32 gets a 4-byte tag
  • b16 16: every Option carries a separate discriminant, so both are one word larger than their payload
  • c8 4: Option<i32> reuses the sign bit of the integer as its None marker
  • d8 8: None reuses the null pointer that Box never produces, while i32 needs a separate tag
Explanation:Measured: 8 8. Box<i32> is a non-null pointer, and the compiler uses that forbidden bit pattern (the "niche") to represent None, so Option<Box<i32>> is exactly one pointer wide. i32 uses every one of its 2^32 bit patterns, so there is no spare value for None; the discriminant goes in a separate byte and the whole thing is padded to 8 bytes because of the 4-byte alignment. The same niche trick applies to &T and Rc<T>.
Rs Memory Smart Pointers UnsafeDifficulty 2
use std::rc::Rc;
fn main() {
    let a = Rc::new(String::from("x"));
    let b = Rc::clone(&a);
    let v = vec![a.clone(), a.clone()];
    println!("{} {}", Rc::strong_count(&a), Rc::ptr_eq(&a, &b));
    drop(v);
    drop(b);
    println!("{}", Rc::strong_count(&a));
}

What is printed?
  • a3 true then 0: the original a is not counted, and dropping b releases the last reference
  • b4 false then 1: Rc::clone copies the String into a fresh allocation, so the pointers differ
  • c1 true then 1: the counter tracks distinct owners, and v holds two handles to the same owner
  • d4 true then 1: each clone bumps one shared counter, and the drops bring it back down
Explanation:Measured output: 4 true / 1. Rc::clone (and .clone() on an Rc) never copies the String; it increments the strong count and hands back another pointer to the same heap block, which is why Rc::ptr_eq is true. a, b and the two elements of v are four strong handles. Dropping v releases two, dropping b releases one, leaving a alone with a count of 1. The value itself is freed only when that last count reaches 0.
Rs Memory Smart Pointers UnsafeDifficulty 2
use std::cell::RefCell;
fn main() {
    let c = RefCell::new(vec![1]);
    let r1 = c.borrow();
    let r2 = c.borrow();
    println!("{} {}", r1.len(), r2.len());
    let m = c.borrow_mut();
    println!("{}", m.len());
}

What happens when this runs?
  • aPrints 1 1 then 1: RefCell allows any mix of borrows as long as they happen on one thread
  • bCompile error E0502: r1 and r2 are still alive when borrow_mut asks for exclusive access
  • cPrints 1 1, then panics with already borrowed since two shared Ref guards are still alive
  • dPrints 1 1 then 1: r1 and r2 are released automatically as soon as borrow_mut is called
Explanation:Measured: 1 1 is printed, then the program panics with RefCell already borrowed. RefCell moves the borrow rules from compile time to run time: it keeps a borrow counter, and borrow_mut checks that no Ref or RefMut guard is alive. r1 and r2 still live until the end of main, so the check fails and panics. try_borrow_mut returns an Err instead of panicking, and dropping the guards (or scoping them in a block) before borrow_mut makes the code run through.
Rs Memory Smart Pointers UnsafeDifficulty 1
use std::cell::Cell;
fn bump(c: &Cell<u32>) {
    c.set(c.get() + 1);
}
fn main() {
    let c = Cell::new(1);
    bump(&c);
    bump(&c);
    println!("{} {} {}", c.get(), c.take(), c.get());
}

What does this print?
  • aCompile error E0596: bump receives a shared reference, so set cannot mutate through it
  • b3 3 3: take returns a copy of the value and leaves the cell unchanged
  • c3 3 0: set works through &Cell, and take swaps the value out for u32::default()
  • d1 1 0: mutations made through a shared reference are discarded when bump returns
Explanation:Measured: 3 3 0. Cell<T> is the simplest form of interior mutability: set and get take &self, so a plain &Cell<u32> is enough to change the value. There is no runtime borrow flag either, since get copies the value out and set copies a new one in (this is why Cell::get requires T: Copy). take returns the current value and stores Default::default(), which for u32 is 0, hence the final 0.
Rs Memory Smart Pointers UnsafeDifficulty 2
struct D(&'static str);
impl Drop for D {
    fn drop(&mut self) { println!("drop {}", self.0); }
}
fn main() {
    let a = D("a");
    let b = D("b");
    std::mem::forget(a);
    drop(b);
    println!("end");
}

What is the complete output?
  • adrop b then end: forget never runs a's destructor, drop runs b's right away
  • bdrop b, end, drop a: forget only postpones a's destructor to the end of main
  • cdrop a, drop b, end: both calls consume their argument, and consuming a value runs Drop
  • dCompile error E0382: a is used after forget moved it, since Drop types cannot be forgotten
Explanation:Measured output: drop b / end. std::mem::drop is literally fn drop<T>(_x: T) {}: it takes ownership and lets the value fall out of scope right there, so b's destructor runs at that line. std::mem::forget also takes ownership but deliberately never runs the destructor, so drop a is never printed and any heap memory a owned would leak. Leaking is considered safe in Rust (no memory unsafety), which is exactly why forget is not an unsafe function.

Test yourself against the 3750-question Backend bank.

Start interview