Rust Functions and Ownership

Rust Functions and Ownership

In Rust, ownership rules apply to functions as well as variable bindings. These ownership principles allow Rust to achieve memory safety while passing values into and returning values from functions.

Functions and Ownership Rules

Let’s start by reviewing the ‘rules of ownership‘ in Rust. Recall that these rules are general principles that are designed to help Rust achieve memory safety:

  1. Values have owners to which they are bound.
  2. When an owner is out of scope, the value is cleared from memory.
  3. A value can only have one owner at a time.
  4. Primitive types get copied; non-primitive types get moved.

All of these rules apply when working with functions, just as they do with variable bindings. Let’s see this in action for both possible cases: passing values into a function and returning values from a function.

Ownership and Passing Values Into a Function

When we pass a value into a function, primitive types get copied while non-primitive types get moved. This was covered in-depth in Copy Types and Move Types, but we will see what happens in both cases when we pass each type into a function.

Passing a Copy Type Into a Function

There isn’t much to worry about when we pass a copy type into a function because both the copy and the original variable both retain ownership of their own version of the data on the stack.

fn print_num(num:i32){
    println!("Value of num inside the function: {}",num);
}

let num = 42;
print_num(num);

println!("num is an i32 so it was copied and can still be called: {}", num);

Standard Output:

Value of num inside the function: 42
num is an i32 so it was copied and can still be called: 42

We are able to call num even after it was passed into the function because a copy was made. Let’s see what happens if we pass a move type into the function.

Passing a Move Type Into a Function

When we pass a move type into a function, the original variable loses ownership of the data.

Let’s look at an example of passing a vector into a function. A vector is non-primitive, so it is a move type.

The following example compiles without any error because we only call my_vec once:

fn print_vec(vec: Vec<i32>) {
    println!("Vector inside the function: {:?}",vec);
}

let my_vec = vec![1,2,42];
print_vec(my_vec);

Standard Output:

Vector inside the function: [1, 2, 42]

However if we try to call my_vec again, we will get an error:

fn print_vec(vec: Vec<i32>) {
    println!("Vector inside the function: {:?}",vec);
}

let my_vec = vec![1,2,42];
print_vec(my_vec);

println!("Attempting to print my_vec again: {:?}", my_vec);

Standard Error:

7  |     let my_vec = vec![1,2,42];
   |         ------ move occurs because `my_vec` has type `Vec<i32>`, which does not implement the `Copy` trait
8  |     print_vec(my_vec);
   |               ------ value moved here
9  |
10 |     println!("Attempting to print my_vec again: {:?}", my_vec);
   |                                                        ^^^^^^ value borrowed here after move

Resolving Compilation Issues With Move Types by Returning

We can resolve this issue by returning the same variable binding that was passed into the function. This works because ownership is returned from the function. There are two extra steps required:

  1. We have to return the vector that was passed into the function.
  2. We need to reassign the vector to a new variable with the same name as the original.

    // The vector is used by print_vec() and then returned:
    fn print_vec(v: Vec<i32>) -> Vec<i32>{
        println!("Vector inside the function: {:?}",v);
        v
    }

    let my_vec = vec![1,2,42];

    // We need to reassign the output of print_vec to my_vec:
    let my_vec = print_vec(my_vec); 

    // Ownership is transferred from the function so we can print again:
    println!("Attempting to print my_vec again: {:?}", my_vec);
    

Standard Output:

Vector inside the function: [1, 2, 42]
Attempting to print my_vec again: [1, 2, 42]

By Rust standards, this is quite ugly and tedious. It is much cleaner and easier to do this by borrowing, which we will cover in the next tutorial.

Ownership and Returning Values From a Function

In the previous example, we were able to resolve compilation issues by returning the input parameter from the function. This works because when one or several values are returned from a function, ownership of those values gets transferred to the calling variable or function.

Let’s look at an example using multiple returning values. The function divide_vec_in_two() takes a single vector as an argument and slices it into two vectors. It returns the two vectors and then gives ownership of the two returned vectors by binding them to vec1 and vec2:

fn main() {
    fn divide_vec_in_two(v: Vec<i32>) -> (Vec<i32>, Vec<i32>) {
        println!("Original vector: {:?}",v);
        (v[..2].to_vec(), v[2..].to_vec())
    }

    let my_vec = vec![1,2,36,42];
    let (vec1, vec2) = divide_vec_in_two(my_vec);
    println!("First vector slice: {:?}", vec1);
    println!("Second vector slice: {:?}", vec2);
}

Standard Output:

Original vector: [1, 2, 36, 42]
First vector slice: [1, 2]
Second vector slice: [36, 42]

The important thing here is the flow of ownership:

  • The original vector my_vec has ownership until it is passed into the function.
  • The function has ownership of my_vec and the two vector slices.
  • The function transfers ownership of the vector slices when they are bound to the new variables vec1 and vec2.
  • vec1 and vec2 retain ownership of the slices until they go out of scope.

If we wanted to also retain the original vector, we could simply add another vector return type. This would enable the original vector to be reused after the function call.

fn main() {
    fn divide_vec_in_two(v: Vec<i32>) -> (Vec<i32>, Vec<i32>, Vec<i32>) {
        println!("Original vector: {:?}",v);
        (v.to_vec(), v[..2].to_vec(), v[2..].to_vec())
    }

    let my_vec = vec![1,2,36,42];
    let (my_vec, vec1, vec2) = divide_vec_in_two(my_vec);
    println!("First vector slice: {:?}", vec1);
    println!("Second vector slice: {:?}", vec2);
    println!("The original vector can be called again: {:?}", my_vec);
}

Standard Output:

Original vector: [1, 2, 36, 42]
First vector slice: [1, 2]
Second vector slice: [36, 42]
The original vector can be called again: [1, 2, 36, 42]