What is the difference between passing a value to a function by reference and passing it "by Box":
fn main() { let mut stack_a = 3; let mut heap_a = Box::new(3); foo(&mut stack_a); println!("{}", stack_a); let r = foo2(&mut stack_a); // compile error if the next line is uncommented // println!("{}", stack_a); bar(heap_a); // compile error if the next line is uncommented // println!("{}", heap_a); } fn foo(x: &mut i32) { *x = 5; } fn foo2(x: &mut i32) -> &mut i32 { *x = 5; x } fn bar(mut x: Box<i32>) { *x = 5; }
Why is heap_a
moved into the function, but stack_a
is not (stack_a
is still available in the println!
statement after the foo()
call)?
The error when uncommenting println!("{}", stack_a);
:
error[E0502]: cannot borrow `stack_a` as immutable because it is also borrowed as mutable --> src/main.rs:10:20 | 8 | let r = foo2(&mut stack_a); | ------- mutable borrow occurs here 9 | // compile error if the next line is uncommented 10 | println!("{}", stack_a); | ^^^^^^^ immutable borrow occurs here ... 15 | } | - mutable borrow ends here
I think this error can be explained by referring to lifetimes. In the case of foo
, stack_a
(in the main
function) is moved to function foo
, but the compiler finds that the lifetime of the argument of the function foo
, x: &mut i32
, ends at end of foo
. Hence, it lets us use the variable stack_a
in the main
function after foo
returns. In the case of foo2
, stack_a
is also moved to the function, but we also return it.
Why doesn't the lifetime of heap_a
end at end of bar
?
Besides, the pass by value requires more memory than pass by reference. Time requirement is one other difference between pass by value and pass by reference. Pass by value requires more time as it involves copying values whereas pass by reference requires a less amount of time as there is no copying.
On the other hand, when you pass a value-type parameter to a function by reference, the changes you make to that parameter inside the function will change the original data.
pass by value means that when we pass a variable to a function, it is copied over onto a new one and a change inside of the function scope will not be reflected outside of it
When you pass by reference, you are passing the memory location of the variable to the function, and any changes you make inside a function will affect that location in memory and will therefore persist after the function completes. A couple of examples of reference types are Arrays and Delegates.
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.
When you pass a boxed value, you are moving the value completely. You no longer own it, the thing you passed it to does. It is so for any type that is not Copy
(plain old data that can just be memcpy
’d, which a heap allocation certainly can’t be). This is how Rust’s ownership model works: each object is owned in exactly one place.
If you wish to mutate the contents of the box, you should pass in a &mut i32
rather than the whole Box<i32>
.
Really, Box<T>
is only useful for recursive data structures (so that they can be represented rather than being of infinite size) and for the very occasional performance optimisation on large types (which you shouldn’t try doing without measurements).
To get &mut i32
out of a Box<i32>
, take a mutable reference to the dereferenced box, i.e. &mut *heap_a
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With