Rust Basics - Module 2: Ownership
Part 2 of 5 in Rust BasicsIn the previous blog post of this series, we have learned about the Variables, Types, Functions and Control Flow which are very important to get start with any programming language.
Today, we are going to discuss little deeper and most important topic in all of Rust i.e., Ownership. Everything else like borrowing, lifetimes, concurrency safety is built on top of what Ownership is.
Part 1: The Problem Rust is Solving
In C, memory bugs fall into a few categories:
Use-after-free: You free a buffer and then dereference it.
Double-free: Two code paths both call free on the same pointer.
Memory Leak: Nobody calls free at all.
Dangling Pointer: a pointer outlives the data it points to or in simpler word accessing a pointer beyond its scope.
Languages like C++ and Java solve this with a Garbage Collector - a runtime process that tracks all live references and frees memory when nothing points to it anymore. It brings GC pauses, heap pressure, and no control over layout or freeing timing.
Rust's answer is different: Encode the rules of memory ownership into the type variables, and have the compiler enforce them at compile time. Zero runtime cost, zero garbage collection, zero undefined behaviour.
And the rules are called the "Ownership System".
Part 2: The Ownership Rules
There are three axioms/rules. Everything else is a consequence of them.
Every value in Rust has exactly one owner i.e a Variable.
When the owner goes out of scope, the value is dropped (memory freed).
There can only be one owner at a time.
If any of the rules are violated, the program won't compile. And these features of ownership won't slow down our program as well since these will be checked at compile time itself, so if compilation passes, we can safely assume our program is memory bug free.
Part 3: Stack v/s Heap
Before going deep dive into Ownership, let's discuss where the values and data live. Rust explicitly holds these some data structures based on type system.
Many programming languages don't require you to think about the stack and the heap very often. But in a systems programming language like Rust, whether a value is on the stack or the heap affects how the language behaves and why you have to make certain decisions.
Stack
Stack stores the values in the order it gets them, and remove in opposite, or we can say it follows LIFO (Last In First Out) principle.
All the data stored on stack must have a known, fixed size at compile time. Data with an unknown size or a size that might change must be stored on the heap instead.
On function return, its entire stack frame is gone i.e., all local variables on it are freed automatically.
Fast Access and No Runtime allocation.
Heap
The heap is less organized: When you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot in the heap (which is big enough), marks it as being in use, and returns a pointer, which is the address of that location.
Request memory at Runtime via an Allocator,
Persists until explicitly freed.
Stack stores a pointer to it.
In Rust, types that are entirely stack-resident implement the Copy trait (more on this below) - integers, booleans, floats, tuples, fixed arrays.
So what is the Copy trait? In Rust, when we assigns a value of one variable to another variable, if the value is copied to another variable instead of giving a reference/pointer, then that type has a Copy trait. Types that has own heap memory are like String , Vec<T> , Box<T> - do not implement Copy. This distinction drives the move semantics which we are going to learn now.
Part 4: Move Semantics
fn main() {
let num1 = 5;
let num2 = num1; // Now "5" is copied to num2 as well
println!("{} {}", num1, num2); // Prints 5, 5
let s1 = String::from("hello");
let s2 = s1; // Ownership moved from s1 to s2
println!("{}", s1); // Compile Error: borrow of moved value: `s1`
}Why does this fail?
Let's look closer at what String actually is in memory:
As discussed above, String memory will be allocated in the Heap, but a pointer to that Heap location is stored in the stack (4th point in Part 3 Heap section). Now let's look closer at what String actually is in memory:
A String on the stack is a three-field struct:
a pointer to heap data
a length
a capacity
So, when we write let s2 = s1 , Rust does not deep-copy the heap data. Instead it copies the three stack fields (pointer, len, cap) to s2, and then immediately invalidates s1 . This is a Move. Ownership of the heap data transferred from s1 to s2 . And there is now exactly one owner to the memory location in Heap i.e s2(Axiom 1 in ownership rules).
Why does Rust do this instead of Copying?
Because copying heap data is an O(n) operation. Rust never silently does expensive things. If we want a deep copy, we must ask for it explicitly with .clone() .
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicit deep copy - heap data duplicated
println!("{}", s1); // s1 is valid and prints "hello"
println!("{}", s2); // s2 is ALSO valid and prints "hello", has it's own copy
}.clone() is a signal in code review: "this is an allocation, this costs memory and time." It is intentional, never hidden.
Part 5: Copy Types - the exception
For stack-only types, there is no heap to worry about i.e., copying is just copying bits. Rust mark these types with the Copy trait, and for them, assignment copies instead of moving.
let x: i32 = 5;
let y = x; // x is Copy - this is a bitwise copy, not move
println!("{}", x); // x is still valid
println!("{}", y); // y has it's own copyThe Copy types include: all integer types, f32/f64, bool, char, tuples and arrays composed entirely of Copy types.
String is NOT Copy because it owns heap memory. &str (a string reference - not owner) IS Copy, because it is just a pointer, it does not own the heap data.
Part 6: Ownership and Functions
What happens when we pass the values to functions as Parameters/Arguement, a Move happens, not just an assignment.
fn print_string(s: String) { //s become the owner now
println!("{}", s);
} // s dropped here and heap freed
fn main() {
let s = String::from("hello");
print_string(s); // ownership moves into the function
println!("{}", s); // compile error: borrow of moved value: `s`
}The function print_string took ownership of s . When it returned, s was dropped. The caller no longer has it.
We have two ways out of this:
Brute-force
Borrowing (will discuss in next module)
Option 1: Return Ownership
The brute-force option is to return the ownership back to the original owner who passed it.
fn print_string(
s: String // Takes the ownership from the Caller
) -> String /* Returning String returns ownership to the caller back */ {
println!("{}", s);
s
}
fn main() {
let s = String::from("hello");
let s = print_string(s); // get ownership back
println!("{}", s); // valid now
}This is intentionally verbose. Rust is showing us the cost. The real solution is lending without transferring ownership i.e Borrowing.
Option 2: Borrowing
If ownership is the rule, borrowing is the exception that makes the rule practical. Without borrowing every function call would consume its arguments and it’s painful.
Borrowing lets you hand data to a function/variable without giving up ownership of it.
Example
let s = String::from("hello");
let r = &s // r borrows s - s still owns the heap data
println!("{}", r); // Valid, prints "hello"
println!("{}", s); // Valid, prints "hello"& let us borrow the heap from s to r , then r can work on the borrowed data. We will discuss in detailed in next module
Part 7: Drop - Automatic Cleanup
When a value's owner goes out of scope, Rust calls it's drop function automatically. This is analogous to a C++ destructor, except in Rust it run deterministically, at a known point in the code, not whenever a Garbage Collector (GC) decides to collect.
{
let s = String::from("hello"); // heap allocated
// Use 's' as per the use-case
// <-- 's' goes out of scope here. Drop called, heap freed.
}We never call drop manually (well, std::mem::drop(s) exists for forced early drops, but it's uncommon in practice). The compiler inserts the call automatically.
This is how Rust achieves C-level performance without a Garbage collector, memory is freed as soon as it's no longer needed, deterministically, with zero runtime bookkeeping.
Conclusion
Ownership is Rust's core bet: instead of managing memory at runtime (GC) or leaving it to the programmer (C/C++), encode the rules into the type system and let the compiler enforce them. The three axioms: one owner, drop on scope exit, no simultaneous owners - are simple. But everything that follows (borrowing, lifetimes, Arc, Mutex) is just the compiler asking "does this respect ownership?" at increasingly complex levels.
In the next post, we'll look at Borrowing and References - the mechanism that lets you use data without taking ownership of it, which is how real Rust code avoids the verbose "return ownership back" pattern we saw in Part 6.
Enjoyed this post?
2 reactions