Intro
One way to avoid borrowing completely is to make copies, which is the focus of this article:
- How memory is managed in Rust using the "stack".
- How the
Copy
marker trait is used to denote "trivial" types that are implicitly copied. - How the
Clone
trait allows you to avoid borrowing completely, but at the expense if copying data around. - "Moving" rules.
Stack Memory Management
All memory is managed by a stack per thread in Rust. When calling functions, the arguments are copied onto the stack. During execution of the function, any local variables are pushed onto the stack, too. Finally, when the function returns, any local variables and function arguments are popped off the stack and replaced with the return value(s) of the function.
fn main() {
let num = 2;
let even = is_even(num);
print!("is_even={even} num={num}")
}
fn is_even(num: i32) -> bool {
num % 2 == 0
}
In this simple example, main
is called with no arguments, so the stack is empty. Technically this isn't entirely true, since a "program counter" is always recorded into the stack on every function call so the code knows where to resume execution on return, but we will gloss over that detail for now.
The first line of code in main
pushes a local variable num
onto this stack and assigns it the value 2
. So, now we have a stack of one value: [2]
.
TODO: Display a stack visual
The second line of code copies num
onto the stack and calls is_even
. So, now our stack has two values: [2, 2]
.
TODO: Display a stack visual
The is_even
function checks if the top of the stack is an even value, and then returns this fact. So, at the end of this function, the stack is now [2, true]
(the 2
function argument gets popped and the function result true
is pushed).
TODO: Display a stack visual
The result of the function is stored in a local variable even
, and so the stack continues to contain [2, true]
after the second line completes.
TODO: Display a stack visual
The final line of code prints both the local variables stored on the stack.
When main
returns, the two local variables are popped off the stack.
TODO: Display a stack visual
The Copy
trait
In our first example, we worked with the i32
type. Let's replace this with our own type the wraps the i32
and see what happens:
#[derive(Debug)]
struct Num(i32);
fn main() {
let num = Num(2);
let even = is_even(num);
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: Num) -> bool {
num.0 % 2 == 0
}
The #[derive(Debug)]
and extra :?
s in the print!
statement are to avoid implementing the Display
trait, you can ignore that for now.
Overall, this is the same code as our first example, but the i32
is wrapped into a Num
struct... but the compiler gives us this error:
error[E0382]: borrow of moved value: `num`
--> src/main.rs:7:34
Note that this isn't the entire error message, since the Rust compiler tries very hard to give you more useful information on how to fix the problem.
So, what is different about the Num
struct vs the i32
? The difference is that i32
"implements" the Copy
trait. So, let's change Num
so it implements the Copy
trait and see if it compiles:
// Notice the "Clone" is also derived
#[derive(Debug, Clone)]
struct Num(i32);
// Notice the "Copy" implementation:
impl Copy for Num {}
fn main() {
let num = Num(2);
let even = is_even(num);
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: Num) -> bool {
num.0 % 2 == 0
}
The Copy
trait is a "marker" trait and thus it has no implementation body. In other languages this pattern of implementing a trait which has no code associated with it, might be called a "marker interface" or "tagging interface".
Therefore, the impl Copy for Num {}
is all that is needed to implement this trait. Note that this trait requires the Clone
trait to be implemented, so I added that to the #[derive(Debug, Clone)]
.
Another way to implement both Clone
and Copy
is to derive this using: #[derive(Debug, Copy)]
.
Let's make one small change to the Num
struct so it has an English string that describes the number:
// Notice the extra `String` field:
#[derive(Debug, Clone)]
struct Num(i32, String);
impl Copy for Num {}
fn main() {
let num = Num(2, "two".into());
let even = is_even(num);
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: Num) -> bool {
num.0 % 2 == 0
}
Now we get this error:
error[E0204]: the trait `Copy` cannot be implemented for this type
--> src/main.rs:4:15
This is because all fields of the struct must implement Copy
, but the String
type does not implement this.
Although it would be handy if String
implemented Copy
, it is also a potentially computationally expensive operation, since a String
is not a fixed size. Imagine if you had a million character string that you passed around. Every time it was passed into a function, your program would need to copy the million characters.
However, for simple types (like enums with no associated values), it is highly recommended that you just #[derive(Copy)]
.
Cloning
One way to solve the problem of structures that can't implement Copy
is to explicitly clone()
them.
// Notice the Clone is derived and NOT Copy
#[derive(Debug, Clone)]
struct Num(i32, String);
fn main() {
let num = Num(2, "two".into());
// Notice the call to `.clone()`
let even = is_even(num.clone());
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: Num) -> bool {
num.0 % 2 == 0
}
Although this is a great "hammer" to have in your toolbox, you do need to be aware of the performance implications in case the thing being copied around is large.
Cloning is also not great for any containers that are managing resources. For example, you probably shouldn't be clone()
ing a file descriptor or a mutex.
To manage resources, it is recommended to implement the Drop
trait so that resources can automatically be "closed" just before their memory is released. The Drop
trait is analogous to implementing Java's AutoCloseable
interface, and using a try-with-resource
block to ensure the resource is "closed", or Golang's defer
statement.
Moving (aka shallow copy)
All datatypes in Rust can be "moved", which is just a fancy name for taking a shallow bit-for-bit copy of a datatype. Moving does come with a big restriction though: you can't continue to use the value, and thus in this final example, the is_even()
function was modified so it returns the Num
that was passed into it.
#[derive(Debug)]
struct Num(i32, String);
fn main() {
let num = Num(2, "two".into());
// num is "moved" in here:
let (even, num) = is_even(num);
// ...which means we can NOT print it any more
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: Num) -> (bool, Num) {
// num is "moved" out here:
(num.0 % 2 == 0, num)
}
If you try to remove the re-assignment to num:
#[derive(Debug)]
struct Num(i32, String);
fn main() {
let num = Num(2, "two".into());
// notice that we no longer re-assign to num!
let (even, _) = is_even(num);
// ...which means we can NOT print it any more
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: Num) -> (bool, Num) {
// num is "moved" out here:
(num.0 % 2 == 0, num)
}
You will get this error:
error[E0382]: borrow of moved value: `num`
--> src/main.rs:9:34
This is because line 7 is "stealing" the num
, and thus all subsequent attempts to use num
(like when it performs "print") will fail.
Rust enforces this constraint, since a shallow copy could be stored in a global variable, but there is now multiple shallow copies that are potentially sharing data, since the "deep" parts of that data did not get copied. How would the borrow checker know when the "deep" parts of the data are no longer used?
Recursive Data Structures: Box
In order for the compiler to know how much space is needed to store a structure on the stack, it needs to know the exact size of that structure. Therefore if you try to define a recursive data structure like this:
#[derive(Debug)]
struct Num(i32, Option<Num>);
fn main() {
let num = Num(2, None);
print!("num={num:?}")
}
You will get this error:
error[E0072]: recursive type `Num` has infinite size
--> src/main.rs:2:1
To get around this limitation, you instead need to have the memory stored on the "heap", and instead just have a unique pointer stored on the stack. You can use the Box
datatype in Rust to do this.
Let's try to modify the recursive data structure by using a Box
:
#[derive(Debug)]
struct Num(i32, Option<Box<Num>>);
fn main() {
let num = Num(2, None);
print!("num={num:?}")
}
Note that all the same ownership constraints are the same for a Box
. However, a Box
s size doesn't need to be known at compile time since it dynamically allocates memory on the heap. Recursive data structures are just one example of where this is needed, but this is also needed when working with dyn
traits, but this series won't get into the details of that.
Copy & Clone Recap
To recap:
- Simple types (in Java these are called "primitives") should implement
Copy
in order to get implicit copy semantics. - Other types, should implement
Clone
, and you can use the.clone()
"hammer" to avoid borrowing. Notice how using this "hammer" is very explicit in our code, since it requires calling.clone()
. - Any non-memory resource should not implement
Clone
and should instead implement theDrop
trait to have the resource automatically released. - Rust allows "moving" values to make shallow clones, and if you use this, you will need to return the "moved" value if you want to subsequently make use of it.
To learn how to actually borrow values, please go to Part 2
Top comments (1)
Thanks and I hope to see UNSAFE tutorial