Patrick Desjardins Blog
Patrick Desjardins picture from a conference

Rust: Structure Memory when Using a Function

Posted on: 2022-11-04

I recently learned about the tool called clippy that is available if you use cargo. It provides lint information on your Rust code to ensure you are using good practices but also helps to use Rust idiomatic and consistent code. For example, one recommendation was to not use .clone() on one of my structures. The suggestion made me think. I was passing the structure into a few functions, and I wanted to ensure no function was modifying the content.

Coming from a JavaScript/TypeScript background, I am cloning and passing the clone to have immutability. However, in Rust, passing a structure in a function does not give a reference in memory to that structure. Nor that passing a structure into a function allows the function to mutate. Rust has two explicit keywords: mut and & to allow mutation and to pass by reference.

Here is a short code that illustrates how a structure is preserved. But, first, let's create a structure.

#[derive(Copy, Clone)]
struct Test {
    x: u32
}

Second, let's create a main function that will create a structure we do not want to modify.

fn main() {
    // Create value and print its memory address
    let t = Test {x:1};
    println!("Original t: {:p}", &t);

    // Call the mutate function
    let new_t = mutate(t);

    // Print the values
    println!("{}",t.x);
    println!("{}",new_t.x);
}

The main function calls the mutate and returns a new instance of the struct. The expectation is that t remains with the value 1 and that the returned value will be 100.

fn mutate(t_param: Test) ->Test {
    // Create a variable that is `x` but marked that it can be mutated
    let mut new_test = t_param;

    // Print memory address
    println!("t_param: {:p}", &t_param);
    println!("new_test: {:p}", &new_test);

    // Mutate the value from 1 to 100
    new_test.x = 100;
    new_test
}

The mutate function cannot change the value of t_param.x without the mut keyword. Rust will throw a compilation error saying that the variable must be mutable. Adding mut to a new variable creates new variables. The memory prints below the assignment show two addresses, which means that the variable passed by the parameter (t_param) is not using the same memory as new_test.

The program prints:

Original t: 0x7ffcc6ba0e94
t_param:    0x7ffcc6ba0dc0
new_test:   0x7ffcc6ba0dc4
1
100

Moving Ownership

In the mutate function, we moved ownership to another variable by assigning the parameter to a new variable.

let mut new_test = t_param;

The ownership is not transferred because of mut

The same occurs when we assign a variable to a function by parameter. For example, moves ownership in the following code:

let new_test = t_param;

The reason is to avoid having two or more variables with a pointer to a space in memory. It avoids bugs.

The consequence of moving the ownership is that the former variable cannot access anything from the object.

Let's modify the former example and see:

fn main() {
    let mut t = Test {x:1};
    let mut new_test = t;
    println!("t_param: {}", t.x);
    println!("new_test: {}", new_test.x);
    
    t.x = 2;
    new_test.x = 1000;
    println!("t_param: {}", t.x);
    println!("new_test: {}", new_test.x);
    
    println!("t_param: {:p}", &t);
    println!("new_test: {:p}", &new_test);
}

The output is:

t_param: 1
new_test: 1
t_param: 2
new_test: 1000
t_param: 0x7ffd03e227f0
new_test: 0x7ffd03e227f4

We can see that the t_param and new_test can adjust the x without affecting each other.

Function Parameter

What small detail that may have gone unnoticed is the trait above the struct #[derive(Copy, Clone)]. This is required to pass the structure in a function otherwise would create a compilation error with a borrow of moved value message. The copy trait is required because, unlike primitive types, they are copied by default. The clone is required because of the copy trait. You can quickly see what I am saying by clicking in VSCode on the copy, which will lead you to the source code of the trait, and you will see:

#[rustc_unsafe_specialization_marker]
#[rustc_diagnostic_item = "Copy"]
pub trait Copy: Clone {
    // Empty.
}

We can step back and have the code (without trait on the struct). Then, we realize the code does not comply with the borrow of moved value.

struct Test {
    x: u32
}

fn main() {
    let t = Test {x:1};
    let new_test = t;
    println!("t_param: {:p}", &t);
    println!("new_test: {:p}", &new_test);
}

Adding #[derive(Copy, Clone)] makes the compiler happy. Once again, the reason is that we are moving from t to new_test, which does a copy.

The output is with two different addresses:

t_param: 0x7ffda950bdb0
new_test: 0x7ffda950bdb4

Reference a.k.a. borrowing

Sometimes, you may want to pass more than one variable without making copies. For example, you are giving a reference, or in the Rust's lingo, is to borrow, which cause an alias to the source of truth. However, there are some additional rules. For example, borrowing into a mutable variable with &mut is only possible once per scope, a.k.a. lifetime. So, for example, you can &mut in a function once and do it again in another function.

#[derive(Copy, Clone)]
struct Test {
    x: u32
}

fn main() {
    let mut t = Test {x:1};
    println!("t_param: {}", t.x);
    println!("t_param: {:p}", &t);
    display(&mut t);
    println!("t_param: {}", t.x);
    println!("t_param: {:p}", &t);
}

fn display(new_test: &mut Test){
    new_test.x = 1000;
    println!("Function x value: {}", new_test.x);
    println!("Function struct ref address: {:p}", new_test);
}

In this example, the output of add the addresses is the same:

t_param: 1
t_param: 0x7ffc5187bd64
Function x value: 1000
Function struct ref address: 0x7ffc5187bd64
t_param: 1000
t_param: 0x7ffc5187bd64

The function shows that the reference address is the same as the original struct with 0x7ffc5187bd64.

Conclusion

Clippy was right! There was no need to call explicitly .clone() because the structure is getting cloned automatically.