This article is a high-level introduction to the topic of Rust ownership. It covers the following areas:
- The stack and the heap
- What is ownership
- What is borrowing
If you want to go deep on the topic of ownership, I recommend the following resources:
One of the main draws of Rust as a language is that it offers memory and thread safety without a garbage collector or runtime. Rust accomplishes this using an ownership model, which the compiler enforces to ensure thread and memory safety at compile-time.
In Rust, ownership is a system of rules that the compiler uses to manage memory. These rules ensure that memory is used safely and efficiently. The concept of ownership can be difficult for some programmers to understand. But, with experience, writing code that follows the rules becomes more natural. It’s important to note that the ownership system is closely tied to the stack and the heap, which are different areas of memory that are used in different ways.
Before this innovation, there were generally two types of languages: languages either had manual or automatic memory management. Now, with Rust, programmers can have memory safety without a runtime—the best of both worlds.
Stack vs. Heap
The stack and the heap are both regions of memory used by a program. The stack stores function call frames and local variables and operates on the last-in, first-out (LIFO) principle. Variables stored on the stack are automatically deallocated when the function exits. Accessing data on the stack is fast and efficient because it is simply a matter of moving the stack pointer.
Data stored on the heap can be accessed randomly and is less efficient than accessing data on the stack because it requires following pointers. On the other hand, the heap is used for dynamic memory allocation and can store variables that have a longer lifetime than the function call. Heap-allocated variables must be deallocated manually.
In general, values that have a fixed and known size are stored on the stack, while values that have a variable or unknown size are stored on the heap. Accessing data on the stack is faster than accessing data on the heap, and allocating space on the heap is more expensive than pushing data onto the stack.
In languages with manual memory management, like C and C++, programmers are responsible for allocating and freeing memory manually as part of their program. This allows programmers to write more efficient code by only allocating memory on the heap if necessary. However, there is a risk that a programming error might cause the program to crash or leak memory. On the other hand, languages like Java and Python allocate memory on the heap for most function calls. The program is less efficient since copying this data is more expensive, but the programmer doesn’t need to worry about allocating and freeing memory. Instead, a garbage collector runs on some interval which cleans up memory that the program is no longer using automatically.
What Is the Ownership System?
Rust’s ownership system helps to manage the stack and the heap without a garbage collector and without the risk of memory safety errors by enforcing rules that programmers must follow. These rules ensure the safe and efficient use of memory. The rules are as follows:
1. Every value has an owner:
Every value in a Rust program is associated with a variable considered to be the owner of that value. When this variable goes out of scope, the value is dropped, and the memory is freed.
For example:
let x = 42; // x is the owner of the value 42
let y = x; // y is now the owner of the value 42
2. There can only be one owner
Each value can have only one owner at a time, ensuring no conflicts or bugs exist caused by multiple variables trying to access or modify the same memory. This rule is enforced by the compiler, ensuring memory and thread safety.
For example:
fn main() {
let x = vec![1, 2, 3];
take_ownership(x);
take_ownership(x);
// error: use of moved value: `x`
}
fn take_ownership(v: Vec<i32>) {
// do something with v
println!("{:?}", v);
}
If we run this snippet, we get the following (incredibly helpful) error message from the compiler:
error[E0382]: use of moved value: `x`
--> src/main.rs:4:20
|
2 | let x = vec![1, 2, 3];
| - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | take_ownership(x);
| - value moved here
4 | take_ownership(x);
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
Because the first function call takes ownership of the variable x
, the second function call violates the second rule: there can only be one owner. Imagine if a pointer to the vector was passed to both functions, then the compiler would not be able to free the memory safely down the road.
But about this note about the `Copy` trait?
What if we try this same manoeuvre but using an integer instead of a Vector:
fn main() {
let x = 42;
take_ownership(x);
take_ownership(x);
// error: use of moved value: `x`
}
fn take_ownership(i: i32) {
// do something with i
println!("{i}");
}
Running this snippet actually works just fine and produces the following output:
42
42
Why does the compiler not complain about this violation of the second rule? If a value can fit into a single machine word—in other words, 8 bytes on a 64-bit architecture—the Rust compiler can cheaply copy the entire data. The data itself is no larger than a pointer to the data. Because the data is entirely copied, the receiving scope can simply free the memory when done with the value.
3. Values Are Dropped When the Owner Goes Out of Scope
While the second rule solves the problems of accessing memory that has already been freed or potentially freeing memory twice, the third rule prevents memory leaks, a problem in which memory is never freed. The amount of resources the program consumes grows over time.
In Rust, variables and values have a scope, the portion of the program where the variable is visible and can be accessed. When a variable goes out of scope, the associated values are automatically dropped, and the memory is deallocated. This ensures that memory is not leaked and there are no memory leaks in the program.
For example:
fn main() {
// new scope
{
let x = 42;
println!("{}", x);
} // x goes out of scope here
println!("{x} is not accessible here");
}
Running this produces the following error:
error[E0425]: cannot find value `x` in this scope
--> src/main.rs:7:16
|
7 | println!("{x} is not accessible here");
| ^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
By enforcing these rules, Rust’s ownership system can prevent common bugs related to manual memory management without a runtime or garbage collector.
Borrowing
Rust’s ownership system also allows for borrowing, in which a value can be borrowed for a specific scope without taking ownership. This means multiple variables can read and access the value without taking ownership, providing a safer and more controlled way of using and manipulating the data.
Borrowing a variable in Rust uses the &
symbol before the variable name. This is called referencing. The borrowed variable remains the owner, and the borrower only gets to read its value. The borrower does not have ownership rights over the variable, so it can’t modify it.
For example:
fn main() {
let s = "hello";
// pass a reference
print_length(&s);
// we can still use s here
println!("{}", s);
}
fn print_length(s: &str) {
println!("'{}' has a length of {}", s, s.len());
}
In this example, we are passing a reference to the variable s
to the function print_length
. This function can read the value of s
but cannot modify it. The variable s
is still accessible and usable within the main function. This allows us to use the variable in multiple parts of the code without transferring ownership every time.
It’s also possible to create mutable references, which allows multiple variables to read and modify a value. This is a helpful pattern but comes with restrictions, such as only one mutable borrow can happen simultaneously. This means that while a variable is mutably borrowed, the original variable cannot be borrowed again.
For example:
fn main() {
let mut s = String::from("Hello");
add_word(&mut s);
println!("{}", s); // prints "Hello world"
}
fn add_word(s: &mut String) {
s.push_str(" world");
}
Conclusion
Rust’s ownership system is a key feature that ensures a program’s safe and efficient memory use. The ownership system enforces three main rules: every value has an owner, there can only be one owner, and values are dropped when the owner goes out of scope. These rules help prevent common bugs such as data races and segmentation faults.
The ownership system also allows for the borrowing of values, which allows multiple variables to read a value without taking ownership of it. This allows for more flexibility and control over how memory is used in a program.
The ownership system can be a steep learning curve for new Rust programmers, but it ultimately helps make Rust programs faster and safer by preventing common memory-related bugs. This can be a significant advantage compared to other languages that rely on garbage collection or manual memory management.
I highly encourage readers to try Rust for themselves and experience the benefits of the ownership system firsthand. The more experience you gain with Rust, the more natural it will become to develop safe and efficient code.
If you enjoyed reading this, please consider sharing this article with someone you think will enjoy it. If I’ve made any errors, or if you have any suggestions or feedback, please let me know! And if you have any questions about the topic, leave a comment below or on Twitter.
Discussion