What is the difference between passing a value to a function by reference and passing it by Box?

Pass-by-value is always either a copy (if the type involved is “trivial”) or a move (if not). Box<i32> is not copyable because it (or at least one of its data members) implements Drop. This is typically done for some kind of “clean up” code. A Box<i32> is an “owning pointer”. It is the sole owner of what it points to and that’s why it “feels responsible” to free the i32‘s memory in its drop function. Imagine what would happen if you copied a Box<i32>: Now, you would have two Box<i32> instances pointing to the same memory location. This would be bad because this would lead to a double-free error. That’s why bar(heap_a) moves the Box<i32> instance into bar(). This way, there is always no more than a single owner of the heap-allocated i32. And this makes managing the memory pretty simple: Whoever owns it, frees it eventually.

The difference to foo(&mut stack_a) is that you don’t pass stack_a by value. You just “lend” foo() stack_a in a way that foo() is able to mutate it. What foo() gets is a borrowed pointer. When execution comes back from foo(), stack_a is still there (and possibly modified via foo()). You can think of it as stack_a returned to its owning stack frame because foo() just borrowed it only for a while.

The part that appears to confuse you is that by uncommenting the last line of

let r = foo2(&mut stack_a);
// compile error if uncomment next line
// println!("{}", stack_a);

you don’t actually test whether stack_a as been moved. stack_a is still there. The compiler simply does not allow you to access it via its name because you still have a mutably borrowed reference to it: r. This is one of the rules we need for memory safety: There can only be one way of accessing a memory location if we’re also allowed to alter it. In this example r is a mutably borrowed reference to stack_a. So, stack_a is still considered mutably borrowed. The only way of accessing it is via the borrowed reference r.

With some additional curly braces we can limit the lifetime of that borrowed reference r:

let mut stack_a = 3;
{
   let r = foo2(&mut stack_a);
   // println!("{}", stack_a); WOULD BE AN ERROR
   println!("{}", *r); // Fine!
} // <-- borrowing ends here, r ceases to exist
// No aliasing anymore => we're allowed to use the name stack_a again
println!("{}", stack_a);

After the closing brace there is again only one way of accessing the memory location: the name stack_a. That’s why the compiler lets us use it in println!.

Now you may wonder, how does the compiler know that r actually refers to stack_a? Does it analyze the implementation of foo2 for that? No. There is no need. The function signature of foo2 is sufficient in reaching this conclusion. It’s

fn foo2(x: &mut i32) -> &mut i32

which is actually short for

fn foo2<'a>(x: &'a mut i32) -> &'a mut i32

according to the so-called “lifetime elision rules”. The meaning of this signature is: foo2() is a function that takes a borrowed pointer to some i32 and returns a borrowed pointer to an i32 which is the same i32 (or at least a “part” of the original i32) because the the same lifetime parameter is used for the return type. As long as you hold on to that return value (r) the compiler considers stack_a mutably borrowed.

If you’re interested in why we need to disallow aliasing and (potential) mutation happening at the same time w.r.t. some memory location, check out Niko’s great talk.

Leave a Comment