Skip to content

← Smart Pointers and Interior Mutability step 1 of 26

Easy Primitives

The stack, the heap, and `Box<T>`

Two small functions over boxed data.

pub fn heaviest(items: Vec<Box<[u8]>>) -> usize
pub fn unbox_sum(v: Vec<Box<i64>>) -> i64

heaviest returns the index of the longest slice; on a tie the lowest index wins; an empty vector returns 0. unbox_sum adds up the boxed integers; an empty vector sums to 0.

The arithmetic is trivial. The point is the Box.

What a Box<T> actually is

A Box<T> is one machine word on the stack holding the address of a T on the heap, plus a promise: when the box is dropped, that heap allocation is freed. That is the whole type. Every other smart pointer in this track — Rc, RefCell, Cow, Weak — is a variation on the same two-part idea: a small handle you can move around cheaply, and a Drop impl that cleans up what the handle points at.

If you came from Java, Python or Go, there was no “who frees this” question: the collector answered it for you, later, invisibly. If you came from C there was a question and no answer — you had to remember. Rust’s answer is ownership: exactly one value owns the allocation, and the compiler knows when that owner dies. Box is the smallest possible expression of it.

The three real reasons to reach for Box

  1. The size is not known at compile time. [u8] — a slice with no length in its type — cannot sit on the stack, because the compiler cannot say how many bytes to reserve. Behind a pointer it can: Box<[u8]> is a fat pointer, two words, address plus length. Recursive types are the same problem and you meet them next.
  2. You want to move a large value without copying its bytes. Moving a Box copies one word regardless of how big the payload is.
  3. You want to own a value by trait rather than by concrete typeBox<dyn Error>, which you have already met in Track 6.

Notice what is not on that list: “because it is on the heap and heap is how I am used to thinking”. Boxing an i64 because Java would have heap-allocated an Integer is the single most common reflex to unlearn here.

*boxed moves the value out — and that is special

Given let b: Box<i64> = Box::new(7);, the expression *b does not merely borrow the seven. It moves it out of the box, and the box is deallocated. Box is the only type in the language with this power; the compiler special- cases it. Try the same thing on an Rc in a few items’ time and you get E0507, cannot move out of an Rc. That asymmetry surprises everyone, so file it away now: * on Box can move; * on every other smart pointer only borrows.

The deliberate trap in the starter

The unbox_sum starter contains a first draft that does not compile:

let owned: Box<i64> = v[i];

Indexing gives you a place, not a value, and moving a non-Copy value out of a place inside a Vec would leave a hole the vector does not know about. That is E0507, cannot move out of index. The compiler is not being fussy: if it allowed this, v‘s destructor would later try to free an allocation that the box you took away has already freed.

There are several honest fixes, and comparing them is the exercise: borrow instead of move (*v[i] copies the i64 out, because i64 is Copy — the box stays put), or consume the whole vector with into_iter() so each Box really is yours. Pick the one that reads best. Both compile.

Signatures a reviewer would reject

Clippy’s default lints police reflexive boxing, and it is worth knowing which shapes they cover — because the coverage is uneven:

  • boxed_localfn f(x: Box<u32>) where a plain u32 would do. You made the caller allocate for nothing.
  • borrowed_box&Box<T> is always wrong. &T is strictly more general and a &Box<T> autoderefs to it anyway, so the extra hop buys the caller nothing but a constraint.
  • box_collection and vec_boxBox<Vec<u8>> and Vec<Box<i64>> put a pointer in front of something that is already a pointer to the heap.
  • unnecessary_box_returns — returning Box<T> for a T that has a known size just forces the caller to unwrap it.

Now the uneven part, and it is worth testing yourself: unbox_sum here takes Vec<Box<i64>>, which is exactly the shape vec_box exists to condemn — and the gate passes it. On 1.95 vec_box inspects struct fields and local bindings, not pub fn parameter types. Do not read a green gate as approval; Vec<Box<i64>> is a bad type here and it is bad on purpose, so that unboxing it is your job.

Loading visualization…