Understanding the Rust ownership model

One of the main USP's of Rust as a language is implicit memory safety. While other low level languages like C pass the responsibility of maintaining this safety onto the developer, Rust handles it at compile time thus making it very hard to write code which isn't memory safe. This is done by the ownership model.

In order to understand the ownership model, you first need to get familiar with the 2 types of memory, i.e. stack and heap memory.

  • Stack memory: Any variable who's size is known at compile time will be stored here.

  • Heap memory: Stores variables whose size not known at compile time.

As a rule of thumb, you can accept that all primitive data type variables will be stored on the stack. And any collections and structures you end up declaring will get stored on the heap with a pointer to them being stored on the stack (Since the size of a pointer is known at compile time, i.e. usize).

If you're not sure why a pointer has definitive size of usize, you can check of this blog post which explains the same : usethesource.hashnode.dev/usize-and-isize-t...

Owner of a value

Each value in Rust will have a single and definitive owner variable. And whenever that variable goes out of scope, that variable will be freed from memory.

So, the allocated memory get's freed based on the scope of it's owner.

Consider the following :

fn some_function(){
   // collection is stored on the heap.
   // pointer to the collection is stored on the stack
   // my_collection is the owner of this allocated memory
   let my_collection = vec![1,2,3]; 
   println!("{:?}", my_collection);
   // Here, my_collection does out of scope. 
   // So all the memory is freed and given back to the memory allocator.
}

Transferring ownership

There are three ways to transfer ownership from one variable to another.

  • Assigning value of one variable to another variable.

  • Passing value to a function.

  • Returning value from a function.

Read the following examples to better understand this:

// type1
fn main(){
   // my_collection is the owner of the 
   let my_collection = vec![1,2,3]; 

   // Ownership of the collection is transferred to new_variable.
   // my_collection doesn't own any value after this line.
   let new_variable = my_collection; 

   // This line will throw a compile time error since my_collection does not own any value.
   println!("{:?}", my_collection);
}
//type2
fn main(){
   // my_collection is the owner of the 
   let my_collection = vec![1,2,3]; 

   // Ownership of the collection is transferred to the function.
   // my_collection doesn't own any value after this line.
   some_function(my_collection)

   // This line will throw a compile time error since my_collection does not own any value.
   println!("{:?}", my_collection);
}
//type3
fn some_function() -> Vector{
   let my_collection = vec![1,2,3]; 

   // Transfer of ownership takes place here.
   return my_collection
}

One exception to this are primitive types. For them, values are just copied and a new value is created. No ownership transfer takes place.

Borrowing values

Consider the following :

fn main(){
   // my_vector is the owner of the collection.
   let my_vector = vec![10,20,30];

   // ownership is transferred to the function.
   print_vector(my_vector);

   // This will give compile error since my_vector doesn't own anything.
   println!("{}", my_vector[0]);
}

fn print_vector(x:Vec<i32>){
   println!("Inside print_vector function {:?}",x);
}

Now, this could cause issues since the above type of functionality is a very common use case while developing software.

Rust supports borrowing of values for this in the form of references.

Borrowing means getting a reference to a value without taking ownership. And it is represented by a & as a prefix to the variable.

The following code shows the use of references:

fn main(){
   // my_vector is the owner of the collection.
   let my_vector = vec![10,20,30];

   // a reference to the value is passed. 
   // ownership remains with my_vector
   print_vector(&v); 

   // No compile time error.
   println!("{}", my_vector[0]);
}
fn print_vector(x:&Vec<i32>){
   println!("Inside print_vector function {:?}",x);
}