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.