Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Smart Pointers
RUST

Smart Pointers

Practice 10 Rust questions covering Box, Rc, Arc, RefCell, ownership, reference counting, and smart pointer use cases.

1. Which smart pointer provides shared ownership by maintaining a reference counter on the heap?

Answer: Rc<T> (Reference Counted) — single-threaded shared ownership with a reference count.

Rc<T> lets multiple variables own the same value. It keeps a reference count on the heap; each Rc::clone(&r) bumps the count, and when the count drops to zero, the value is deallocated.

let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);   // refcount 2 — both a and b own it

Why not always use it? Two constraints:

  • Not thread-safe — the counter isn’t atomic; use Arc<T> (atomic refcount) when sharing across threads.
  • ImmutableRc<T> gives shared immutable access (aliasing). Combine with RefCell<T> (Rc<RefCell<T>>) for interior mutability.

The four choices in the question: Box<T> = single ownership on the heap; Rc<T> = shared single-threaded ownership via refcount; Arc<T> = shared multi-threaded ownership via atomic refcount; RefCell<T> = interior mutability, not ownership. The interview answer: Rc<T> — heap reference counter for single-threaded shared ownership (use Arc<T> across threads).

Answer:

Rc<T> (Reference Counted) — single-threaded shared ownership with a reference count.

Rc<T> lets multiple variables own the same value. It keeps a reference count on the heap; each Rc::clone(&r) bumps the count, and when the count drops to zero, the value is deallocated.

let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);   // refcount 2 — both a and b own it

Why not always use it? Two constraints:

  • Not thread-safe — the counter isn’t atomic; use Arc<T> (atomic refcount) when sharing across threads.
  • ImmutableRc<T> gives shared immutable access (aliasing). Combine with RefCell<T> (Rc<RefCell<T>>) for interior mutability.

The four choices in the question: Box<T> = single ownership on the heap; Rc<T> = shared single-threaded ownership via refcount; Arc<T> = shared multi-threaded ownership via atomic refcount; RefCell<T> = interior mutability, not ownership. The interview answer: Rc<T> — heap reference counter for single-threaded shared ownership (use Arc<T> across threads).

2. What does Box<T> do in Rust?

Answer: It allocates the value on the heap and provides an owned, fixed-size pointer to it.

Box<T> is Rust’s simplest smart pointer: it owns a T stored on the heap while the Box itself sits on the stack (just a pointer-sized value).

let b = Box::new(5);       // 5 lives on the heap
let stack = 5;             // this stays on the stack

When Box goes out of scope, the heap value is dropped and the memory freed (RAII). Key uses:

  • Recursive types: a type that contains itself (linked list, tree) can’t have infinite stack size — Box gives a finite pointer indirection.
  • Large data / trait objects: move a big value or a dyn Trait (unsized type) behind a pointer.
  • Indirection: copy cheaply (just the pointer).

The interview answer: Box<T> allocates T on the heap and owns it through a fixed-size pointer, freeing it automatically on drop.

Answer:

It allocates the value on the heap and provides an owned, fixed-size pointer to it.

Box<T> is Rust’s simplest smart pointer: it owns a T stored on the heap while the Box itself sits on the stack (just a pointer-sized value).

let b = Box::new(5);       // 5 lives on the heap
let stack = 5;             // this stays on the stack

When Box goes out of scope, the heap value is dropped and the memory freed (RAII). Key uses:

  • Recursive types: a type that contains itself (linked list, tree) can’t have infinite stack size — Box gives a finite pointer indirection.
  • Large data / trait objects: move a big value or a dyn Trait (unsized type) behind a pointer.
  • Indirection: copy cheaply (just the pointer).

The interview answer: Box<T> allocates T on the heap and owns it through a fixed-size pointer, freeing it automatically on drop.

3. What is the fundamental difference between Rc<T> and Arc<T>?

Answer: Rc<T> is for single-threaded use (non-atomic refcounting); Arc<T> uses atomic operations for thread-safe reference counting across threads.

Both provide shared ownership via a reference count:

  • Rc<T> — Reference Counted. Updates the counter with plain (non-atomic) increments/decrements. Fast, but not Send/Sync: if you share an Rc across threads, the counter can race → use only on a single thread.
  • Arc<T> — Atomically Reference Counted. Uses atomic operations to bump the count, safe across threads. Slightly slower than Rc (atomic ops have overhead) but thread-safe.
let rc = Rc::new(5);      // single-threaded only
let arc = Arc::new(5);    // shareable with std::thread::spawn

The value itself is still shared immutably in both — pair with Mutex/RefCell for interior mutability. The interview answer: Rc = non-atomic, single-threaded; Arc = atomic, multi-threaded.

Answer:

Rc<T> is for single-threaded use (non-atomic refcounting); Arc<T> uses atomic operations for thread-safe reference counting across threads.

Both provide shared ownership via a reference count:

  • Rc<T> — Reference Counted. Updates the counter with plain (non-atomic) increments/decrements. Fast, but not Send/Sync: if you share an Rc across threads, the counter can race → use only on a single thread.
  • Arc<T> — Atomically Reference Counted. Uses atomic operations to bump the count, safe across threads. Slightly slower than Rc (atomic ops have overhead) but thread-safe.
let rc = Rc::new(5);      // single-threaded only
let arc = Arc::new(5);    // shareable with std::thread::spawn

The value itself is still shared immutably in both — pair with Mutex/RefCell for interior mutability. The interview answer: Rc = non-atomic, single-threaded; Arc = atomic, multi-threaded.

4. What pattern is commonly paired with Rc<T> or Arc<T> to achieve “interior mutability”?

Answer: RefCell<T>/Cell<T> for single-threaded, or Mutex<T>/RwLock<T> for multi-threaded mutation.

Rc<T> and Arc<T> only expose immutable shared access to their contents. To mutate data behind a shared smart pointer, you need interior mutability — mutation through an & reference:

  • Single-threaded: Rc<RefCell<T>> or Rc<Cell<T>> (e.g., a shared graph node whose children can be modified).
  • Multi-threaded: Arc<Mutex<T>> or Arc<RwLock<T>> — the lock provides the exclusive access.
let shared = Rc::new(RefCell::new(0));
*shared.borrow_mut() += 1;      // mutate through shared ownership

let thread_safe = Arc::new(Mutex::new(0));
*thread_safe.lock().unwrap() += 1;

The pattern: shared pointer for ownership + a mutability mechanism for writes. The interview answer: RefCell/Cell (single-thread) or Mutex/RwLock (multi-thread) provide interior mutability inside Rc/Arc.

Answer:

RefCell<T>/Cell<T> for single-threaded, or Mutex<T>/RwLock<T> for multi-threaded mutation.

Rc<T> and Arc<T> only expose immutable shared access to their contents. To mutate data behind a shared smart pointer, you need interior mutability — mutation through an & reference:

  • Single-threaded: Rc<RefCell<T>> or Rc<Cell<T>> (e.g., a shared graph node whose children can be modified).
  • Multi-threaded: Arc<Mutex<T>> or Arc<RwLock<T>> — the lock provides the exclusive access.
let shared = Rc::new(RefCell::new(0));
*shared.borrow_mut() += 1;      // mutate through shared ownership

let thread_safe = Arc::new(Mutex::new(0));
*thread_safe.lock().unwrap() += 1;

The pattern: shared pointer for ownership + a mutability mechanism for writes. The interview answer: RefCell/Cell (single-thread) or Mutex/RwLock (multi-thread) provide interior mutability inside Rc/Arc.

5. How does RefCell<T> enforce borrowing rules compared to standard Rust references?

Answer: RefCell<T> defers the borrow checks to runtime — a violated rule causes a panic, not a compile error.

Normal references (&T, &mut T) are checked by the borrow checker at compile time. RefCell<T> moves the same rules into the running program:

  • .borrow() — get an immutable borrow (multiple allowed, like &T).
  • .borrow_mut() — get a mutable borrow (only one allowed, like &mut T).
  • If you violate the rules at runtime — two simultaneous borrow_mut(), or a borrow_mut() while borrows are active — the program panics (“already mutably borrowed”).
let cell = RefCell::new(5);
let a = cell.borrow();
let b = cell.borrow_mut();   // panics: already borrowed

When to use: when the borrow checker is too strict for your pattern (self-referential data, caches, graph structures) but you still want the guarantees. Trade-off: compile-time safety becomes runtime panic risk. The interview answer: RefCell checks borrow rules at runtime and panics on violation, instead of rejecting at compile time.

Answer:

RefCell<T> defers the borrow checks to runtime — a violated rule causes a panic, not a compile error.

Normal references (&T, &mut T) are checked by the borrow checker at compile time. RefCell<T> moves the same rules into the running program:

  • .borrow() — get an immutable borrow (multiple allowed, like &T).
  • .borrow_mut() — get a mutable borrow (only one allowed, like &mut T).
  • If you violate the rules at runtime — two simultaneous borrow_mut(), or a borrow_mut() while borrows are active — the program panics (“already mutably borrowed”).
let cell = RefCell::new(5);
let a = cell.borrow();
let b = cell.borrow_mut();   // panics: already borrowed

When to use: when the borrow checker is too strict for your pattern (self-referential data, caches, graph structures) but you still want the guarantees. Trade-off: compile-time safety becomes runtime panic risk. The interview answer: RefCell checks borrow rules at runtime and panics on violation, instead of rejecting at compile time.

6. What is the main purpose of the Deref trait coercion mechanism in Rust?

Answer: It lets smart pointers (like Box<T>, Rc<T>, or String) automatically behave like references to their underlying types — e.g., coercing &Box<String> to &str.

Deref coercion is implicit: when a function expects &str (or &[T]) and you pass a smart pointer whose Deref target matches, the compiler inserts the deref chains automatically:

fn takes_str(s: &str) {}
let b = Box::new(String::from("hi"));
takes_str(&b);   // &Box<String> → &String → &str, all implicit

String: Deref<Target = str>, Box<T>: Deref<Target = T>, Vec<T>: Deref<Target = [T]>, etc. This is why &*boxed, .methods() on smart pointers, and generic code over &T all “just work.”

Also note the deref method resolution: when you call a method on Box<String>, the compiler searches the deref chain for the method. The interview answer: Deref coercion implicitly converts &SmartPointer into &Target, letting smart pointers act like their inner types.

Answer:

It lets smart pointers (like Box<T>, Rc<T>, or String) automatically behave like references to their underlying types — e.g., coercing &Box<String> to &str.

Deref coercion is implicit: when a function expects &str (or &[T]) and you pass a smart pointer whose Deref target matches, the compiler inserts the deref chains automatically:

fn takes_str(s: &str) {}
let b = Box::new(String::from("hi"));
takes_str(&b);   // &Box<String> → &String → &str, all implicit

String: Deref<Target = str>, Box<T>: Deref<Target = T>, Vec<T>: Deref<Target = [T]>, etc. This is why &*boxed, .methods() on smart pointers, and generic code over &T all “just work.”

Also note the deref method resolution: when you call a method on Box<String>, the compiler searches the deref chain for the method. The interview answer: Deref coercion implicitly converts &SmartPointer into &Target, letting smart pointers act like their inner types.

7. What is the primary purpose of the std::borrow::Cow (Clone-on-Write) smart pointer?

Answer: It avoids unnecessary clones by borrowing data read-only, then cloning lazily only when mutation is requested.

Cow<'a, B> is an enum: Borrowed(&'a B) or Owned(B::Owned). It lets a function take either borrowed or owned data and only pay for a copy if the data actually gets modified:

fn process(input: &str) -> Cow<str> {
    if input.starts_with("prefix") {
        Cow::Borrowed(input)      // no allocation
    } else {
        Cow::Owned(format!("prefix{input}"))   // only now allocate
    }
}

Internally, .to_mut()/into_owned() triggers the clone when mutation is needed; read-only access (&*cow) uses the borrowed data directly. Use case: functions that may need to modify a string/vec/slice but want to avoid copying when they don’t. The interview answer: Cow borrows until mutation is requested, then clones on demand — avoiding needless allocations.

Answer:

It avoids unnecessary clones by borrowing data read-only, then cloning lazily only when mutation is requested.

Cow<'a, B> is an enum: Borrowed(&'a B) or Owned(B::Owned). It lets a function take either borrowed or owned data and only pay for a copy if the data actually gets modified:

fn process(input: &str) -> Cow<str> {
    if input.starts_with("prefix") {
        Cow::Borrowed(input)      // no allocation
    } else {
        Cow::Owned(format!("prefix{input}"))   // only now allocate
    }
}

Internally, .to_mut()/into_owned() triggers the clone when mutation is needed; read-only access (&*cow) uses the borrowed data directly. Use case: functions that may need to modify a string/vec/slice but want to avoid copying when they don’t. The interview answer: Cow borrows until mutation is requested, then clones on demand — avoiding needless allocations.

8. What is the size of an Option<Box<T>> in memory compared to a raw pointer?

Answer: Exactly the same size as a raw pointer — thanks to the Null Pointer Optimization (NPO).

Option<Box<T>> normally would need a discriminant + the pointer. But Rust’s NPO: since Box<T> can never be null, the compiler uses null (0x0) to represent None. So:

  • Some(box) → the pointer value.
  • None → pointer value 0.

Option<Box<T>> is therefore pointer-sized (8 bytes on 64-bit) — same as *const T — with zero extra storage. The same optimization applies to Option<&T>, Option<NonNull<T>>, Option<Vec<T>>, etc. (any type where a sentinel bit pattern is free). This is why Option is often “free” — no size or runtime cost over the inner pointer type. The interview answer: exactly pointer-sized, because NPO represents None as a null pointer.

Answer:

Exactly the same size as a raw pointer — thanks to the Null Pointer Optimization (NPO).

Option<Box<T>> normally would need a discriminant + the pointer. But Rust’s NPO: since Box<T> can never be null, the compiler uses null (0x0) to represent None. So:

  • Some(box) → the pointer value.
  • None → pointer value 0.

Option<Box<T>> is therefore pointer-sized (8 bytes on 64-bit) — same as *const T — with zero extra storage. The same optimization applies to Option<&T>, Option<NonNull<T>>, Option<Vec<T>>, etc. (any type where a sentinel bit pattern is free). This is why Option is often “free” — no size or runtime cost over the inner pointer type. The interview answer: exactly pointer-sized, because NPO represents None as a null pointer.

9. What does std::cell::Cell<T> provide for interior mutability?

Answer: Interior mutability for Copy types — values are changed by copying in/out, with no references handed out and no runtime borrow checks.

Cell<T> wraps a value and offers get()/set()/replace() that move or copy values:

let c = Cell::new(5);
c.set(10);
let v = c.get();   // v == 10 (Copy)

Key properties:

  • No references: Cell never yields &T or &mut T — it copies values in and out. So the aliasing rules can’t be violated, and no runtime borrow checks are needed (unlike RefCell).
  • Requires Copy: because get() returns by copy, Cell only works with Copy types (u32, bool, references, small structs).
  • Not thread-safe: single-threaded only (not Sync); use Mutex/Atomic across threads.

The interview answer: Cell gives interior mutability for Copy types by copying values in/out, avoiding borrow checks entirely — single-threaded only.

Answer:

Interior mutability for Copy types — values are changed by copying in/out, with no references handed out and no runtime borrow checks.

Cell<T> wraps a value and offers get()/set()/replace() that move or copy values:

let c = Cell::new(5);
c.set(10);
let v = c.get();   // v == 10 (Copy)

Key properties:

  • No references: Cell never yields &T or &mut T — it copies values in and out. So the aliasing rules can’t be violated, and no runtime borrow checks are needed (unlike RefCell).
  • Requires Copy: because get() returns by copy, Cell only works with Copy types (u32, bool, references, small structs).
  • Not thread-safe: single-threaded only (not Sync); use Mutex/Atomic across threads.

The interview answer: Cell gives interior mutability for Copy types by copying values in/out, avoiding borrow checks entirely — single-threaded only.

10. What will happen if you attempt to call .borrow_mut() on a RefCell<T> that already has an active .borrow() reference?

Answer: An immediate runtime panic (already borrowed: BorrowMutError).

RefCell<T> enforces the borrow rules at runtime. When an immutable borrow (.borrow()) is active and you call .borrow_mut():

  • The runtime detects two conflicting borrows (one shared read + one exclusive write).
  • The program panics immediately.
let cell = RefCell::new(5);
let r = cell.borrow();          // active immutable borrow
let m = cell.borrow_mut();      // PANIC: already borrowed

This is the trade-off versus compile-time borrow checking: RefCell accepts code the borrow checker would reject, but pays for it with runtime checks that panic on violation. The interview answer: it panics at runtime (BorrowMutError) — RefCell checks borrow conflicts dynamically.

Answer:

An immediate runtime panic (already borrowed: BorrowMutError).

RefCell<T> enforces the borrow rules at runtime. When an immutable borrow (.borrow()) is active and you call .borrow_mut():

  • The runtime detects two conflicting borrows (one shared read + one exclusive write).
  • The program panics immediately.
let cell = RefCell::new(5);
let r = cell.borrow();          // active immutable borrow
let m = cell.borrow_mut();      // PANIC: already borrowed

This is the trade-off versus compile-time borrow checking: RefCell accepts code the borrow checker would reject, but pays for it with runtime checks that panic on violation. The interview answer: it panics at runtime (BorrowMutError) — RefCell checks borrow conflicts dynamically.

My Private Notes

Notes are auto-saved locally to this device.