Getting Started with Rust Using Rustlings — Part 5: Move Semantics

Jesse Verbruggen
12 min readFeb 16, 2023
Part 5: Move Semantics

In my last part, I reviewed Vectors in Rust. This is Part five of my miniseries on Rust. If you want to read the previous article, you can get to it by following the link below.

In this series, I am going over my process of learning the syntax of Rust with Rustlings. In Part 1, I explain how to install Rust and Rustlings. So if you haven’t yet done that, please navigate here to follow along.

Move Semantics

So this part will be a level up compared to the previous parts. To get started, I will start by explaining the two important concepts for this lesson. The first concept I will explain is Ownership. The second concept explains References and borrowing. Let’s get right into it.

So what is ownership?

I come from programming languages where I left the memory management to the Garbage Collector. In Rust, there is no Garbage Collector. Instead, we have to manage memory ourselves. But don’t worry; Rust was designed to make memory management much simpler than in C++.

Let me explain the two parts of memory we can use to store data. We can access the stack and the heap. Both of these work very differently.

The stack

This is where we store data that is easy to organize. Why is that the case? Because we know the exact size of the data. For example, a boolean always takes up exactly 8 bits, which is 1 byte. That means that when we store a boolean in memory, it will be pushed onto the stack memory.

The data is stored like a vertical stack of plates. When you add a plate on top of the other, it will also be the first you take when you need a plate. The stack works similarly, following the “last in, first out” principle. That makes adding any fixed-size value to the stack very simple and fast.

The heap

Now the heap memory is more like a box of things. These things can have different shapes and sizes, so you need to put the thing wherever you find room for it. But this is a huge box that holds so many things, so to remember where we placed something, we write down where we stored it each time to find it more easily.

In this case, when we put something in the heap memory, we get a pointer to that object that is used to find the thing in the heap memory. That’s called a memory address.

Because whenever we want to find this thing, we need to read the address. It takes longer to access than the stack memory. Now that doesn’t mean the heap should be avoided at all times. The heap memory enables us to store data that might change in size.

Ownership

When our code calls a function, the values we pass into the function get pushed onto the stack. When that function is finished, the values get popped off the stack.

The concept of Ownership helps us manage data on the heap, minimize the amount of duplicate data and clean up unused data to avoid running Out Of Memory (OOM). Let’s take a look at how Rust helps us with Ownership.

fn main() {
let greeting = String::from("hello");

let lenght = greeting.len();

println!("The length of '{}' is {}.", greeting, lenght);
}

So this function works just fine, but let’s extract calculating the length of greeting to another function.

// Does not compile
fn main() {
let greeting = String::from("hello");

let lenght = calculate_length(greeting);

println!("The length of '{}' is {}.", greeting, lenght);
}

fn calculate_length(s: String) -> usize {
s.len()
}

Now, this code does not compile with the following output.

   |
7 | let greeting = String::from("hello");
| - move occurs because `greeting` has type `String`, which does not implement the `Copy` trait
8 |
9 | let lenght = calculate_length(greeting);
| - value moved here
10 |
11 | println!("The length of '{}' is {}.", greeting, lenght);
| ^ value borrowed here after move
|
note: consider changing this parameter type in function `calculate_length` to borrow instead if owning the value isn't necessary
|
14 | fn calculate_length(greeting: String) -> usize {
| ---------------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
9 | let lenght = calculate_length(greeting.clone());
| ++++++++

So the compiler will tell us that what we need to do to fix this code is pass a clone of greeting to the function call. This is because the function will take ownership of the value passed as a parameter. Let’s change that.

// This does compile
fn main() {
let greeting = String::from("hello");

let lenght = calculate_length(greeting.clone());

println!("The length of '{}' is {}.", greeting, lenght);
}

fn calculate_length(s: String) -> usize {
s.len()
}

That looks good, and our code is working. Let’s work out what this looks like in the memory when calculating the length of s.

In this case, we copied the value behind greeting to allow our function to have ownership of its value that it can modify however it wants without affecting the original value of greeting.

The drawback to passing values as a copy is that we have to keep a copy of this value in memory, effectively doubling the memory that our application needs to use. We can use references to borrow ownership of this value to optimise this.

References and borrowing

Let’s start by fixing the code above. We can prefix our variable and the function parameter’s type with an ampersand & to reference our variable.

fn main() {
let greeting = String::from("hello");

let lenght = calculate_length(&greeting);

println!("The length of '{}' is {}.", greeting, lenght);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

Let’s look at how this is represented in memory on this diagram.

What’s happening here is that s is referencing greeting which now contains a pointer to the pointer of greeting. This concept is called borrowing. This way, we don’t need to create any duplicates in memory.

The &greeting syntax lets us create a reference that refers to the value of greeting but does not own it. Because it does not own it, the value it points to will not be dropped when the reference stops being used.

This means that during the runtime of this function, we place this reference onto the stack memory, which makes the execution of this program faster (assuming the compiler doesn’t optimise for us).

Rust has a powerful type system that enforces these concepts at compile time, which can help catch bugs early and make code more reliable. The compiler will check that all references are valid and prevent unsafe memory operations.

A downside is that since we do not take ownership of the value of greeting, the Rust compiler will not allow us to modify its value. But we can pass allow this by using the mut keyword to create a mutable reference.

Mutable References

fn main() {
let mut greeting = String::from("hello");

let lenght = calculate_length(&mut greeting);

println!("The length of '{}' is {}.", greeting, lenght);
}

fn calculate_length(s: &mut String) -> usize {
s.push_str(", world!");
s.len()
}

By making greeting mutable and specifying the mut keyword in both the function call and the function parameter, we take ownership and can modify the value of greeting, without creating a copy. But mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references. Let me explain with three scenarios.

Multiple mutable references

fn main() {
let mut greeting = String::from("hello");

let r1 = &mut greeting; // no problem
let r2 = &mut greeting; // BIG PROBLEM

println!("{}, and {}", r1, r2);
}

In this first scenario, we create two mutable references to the same variable. This creates a big problem because both references will fight over the ownership of the value. The Rust compiler will point out when this happens, helping you fix this problem. If this were allowed, it would mean that this value could change from two references, which might change the value unexpectedly.

Mutable and immutable references

fn main() {
let mut greeting = String::from("hello");

let r1 = &greeting; // no problem
let r2 = &greeting; // no problem

println!("{}, and {}", r1, r2);
}

If we would like to reference this value multiple times, we can, as long as we don’t create a mutable reference, as shown above.

fn main() {
let mut greeting = String::from("hello");

let r1 = &greeting; // no problem
let r2 = &greeting; // no problem
let r3 = &mut greeting; // BIG PROBLEM

println!("{}, {}, and {}", r1, r2, r3);
}

In this third scenario, we are creating both immutable and mutable references. We’re not fighting over ownership this time. Still, because we don’t expect the value of something immutable to change, we cannot create a mutable reference as long as the immutable references are still within scope.

The importance of scope

fn main() {
let mut greeting = String::from("hello");

let r1 = &greeting; // no problem
let r2 = &greeting; // no problem
println!("{}, and {}", r1, r2);
// variables r1 and r2 will not be used after this point

let r3 = &mut greeting; // no problem
println!("{}", r3);
}

Because r1 and r2 are not reused, they will no longer exist within this scope, and we can create a mutable reference at this time.

Dangling references

fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {
let s = String::from("hello");

&s
}

What’s happening in this code is that we are returning a reference but the scope of our variable ends before this reference can be used. The solution here is not to pass a reference but instead pass the entire value to the result.

fn main() {
let string = no_dangle();
}

fn no_dangle() -> String {
let s = String::from("hello");

s
}

This works without any problems. Ownership is moved out, and nothing is deallocated.

Exercises

All right, let’s quickly recap and move on to the exercises.

Memory

We have two types of memory, stack and heap memory. The stack memory contains fixed-size objects, uses “Last In, First Out”, and is quick to access and write. The heap memory contains objects of unknown size that can grow or shrink and is slower to access and write.

Ownership, References and Borrowing

A variable has ownership over its value. Using references, we can borrow this ownership. Immutable references can exist multiple times within the same scope and do not allow the value to be changed. Mutable references cannot coexist with other references within the same scope but can modify the value.

move_semantics1.rs

fn main() {
let vec0 = Vec::new();

let vec1 = fill_vec(vec0);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

vec1.push(88);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
let mut vec = vec;

vec.push(22);
vec.push(44);
vec.push(66);

vec
}

fill_vec is returning a value, of which vec1 take ownership. Then we try to change this value, but this is not a mutable variable. All we need to do is to add the mut keyword to vec1.

fn main() {
let vec0 = Vec::new();

let mut vec1 = fill_vec(vec0);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

vec1.push(88);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

move_semantics2.rs

fn main() {
let vec0 = Vec::new();

let mut vec1 = fill_vec(vec0);

println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec1);

vec1.push(88);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
let mut vec = vec;

vec.push(22);
vec.push(44);
vec.push(66);

vec
}

Make me compile without changing line 13 or moving line 10!

Now, in this case, we are transferring ownership of our vector from vec0 to fill_vec and finally passing it to vec1. After which, we try to access it again from vec0. Since it no longer owns its value, it has nothing to access. What we can do to fix this is to pass a clone.

fn main() {
let vec0 = Vec::new();

let mut vec1 = fill_vec(vec0.clone());

// Do not change the following line!
println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0);

vec1.push(88);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

Using clone, we made a copy of vec0 and kept ownership, which solves this exercise.

move_semantics3.rs

fn main() {
let vec0 = Vec::new();

let mut vec1 = fill_vec(vec0);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

vec1.push(88);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
vec.push(22);
vec.push(44);
vec.push(66);

vec
}

Make me compile without adding new lines — just changing existing lines! (no lines with multiple semicolons necessary!)

In this case, what we can do is pass a mutable reference and a clone of this value to vec1.

fn main() {
let mut vec0 = Vec::new();

let mut vec1 = fill_vec(&mut vec0);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

vec1.push(88);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: &mut Vec<i32>) -> Vec<i32> {
vec.push(22);
vec.push(44);
vec.push(66);

vec.clone()
}

move_semantics4.rs

fn main() {
let vec0 = Vec::new();

let mut vec1 = fill_vec(vec0);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

vec1.push(88);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

// `fill_vec()` no longer takes `vec: Vec<i32>` as argument
fn fill_vec() -> Vec<i32> {
let mut vec = vec;

vec.push(22);
vec.push(44);
vec.push(66);

vec
}

Refactor this code so that instead of passing vec0 into the fill_vec function, the Vector gets created in the function itself and passed back to the main function.

The goal above tells us not to pass an argument to fill_vec but rewrite the code to work. So instead, let’s remove vec0 and fix the initialization of let mut vec.

fn main() {
let mut vec1 = fill_vec();

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

vec1.push(88);

println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

// `fill_vec()` no longer takes `vec: Vec<i32>` as argument
fn fill_vec() -> Vec<i32> {
let mut vec = Vec::new();

vec.push(22);
vec.push(44);
vec.push(66);

vec
}

move_semantics5.rs

fn main() {
let mut x = 100;
let y = &mut x;
let z = &mut x;
*y += 100;
*z += 1000;
assert_eq!(x, 1200);
}

Make me compile only by reordering the lines in `main()`, but without adding, changing or removing any of them.

We cannot have two mutable references within the same scope. If we ensure that y and z do not share the same scope, we will only have one mutable reference at a time.

fn main() {
let mut x = 100;
let y = &mut x;
*y += 100;
let z = &mut x;
*z += 1000;
assert_eq!(x, 1200);
}

Since y is not referenced in the code before z is created, it will not exist within the same scope.

move_semantics6.rs

fn main() {
let data = "Rust is great!".to_string();

get_char(data);

string_uppercase(&data);
}

// Should not take ownership
fn get_char(data: String) -> char {
data.chars().last().unwrap()
}

// Should take ownership
fn string_uppercase(mut data: &String) {
data = &data.to_uppercase();

println!("{}", data);
}

You can’t change anything except adding or removing references.

To transfer ownership, we pass a value. To keep ownership, we pass a reference. In this case, all references are reversed. Let’s fix that by creating a reference for get_char and removing the reference for string_uppercase.

fn main() {
let data = "Rust is great!".to_string();

get_char(&data);

string_uppercase(data);
}

// Should not take ownership
fn get_char(data: &String) -> char {
data.chars().last().unwrap()
}

// Should take ownership
fn string_uppercase(mut data: String) {
data = data.to_uppercase();

println!("{}", data);
}

And that’s it. If you think you don’t yet fully grasp the concepts I explained here or would like to have even more knowledge. I read these two book chapters to help me understand: Ownership and References and Borrowing.

I hope you enjoyed it! You can find my next part on Structs in Rust on the link below.

If I helped you learn more about Rust or you found this interesting, please clap 👏 and share this article to help more people find these articles.

Let’s help each other learn and grow! Peace out✌️

--

--