Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

rust

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?

like image 364
ferux Avatar asked Dec 04 '14 22:12

ferux


People also ask

What is the difference between pass by value and pass by reference?

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.

What happens when you pass a parameter to a function by reference?

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.

What is pass by value in C++?

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

What happens when you pass a variable to a function?

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.


2 Answers

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.

like image 120
sellibitze Avatar answered Oct 10 '22 22:10

sellibitze


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.

like image 40
Chris Morgan Avatar answered Oct 10 '22 22:10

Chris Morgan