Ownership and Lifetimes
This is the chapter that makes Rust click. Ownership is how Rust manages memory
without a garbage collector and without manual free. Lifetimes are how the
compiler proves your references are always valid. Together, they replace the
entire class of memory bugs that plague C programs.
The Three Rules of Ownership
- Every value has exactly one owner.
- When the owner goes out of scope, the value is dropped (freed).
- Ownership can be transferred (moved), not duplicated (by default).
// ownership_basic.rs fn main() { let s1 = String::from("hello"); // s1 owns the String let s2 = s1; // ownership moves to s2 // println!("{}", s1); // ERROR: s1 no longer valid println!("{}", s2); // OK: s2 is the owner now }
Before move:
Stack Heap
+------+---------+ +---+---+---+---+---+
| s1 | ptr --|----| h | e | l | l | o |
| | len=5 | +---+---+---+---+---+
| | cap=5 |
+------+---------+
After move (s1 = s2):
Stack Heap
+------+---------+
| s1 | invalid | (no longer accessible)
+------+---------+
+------+---------+ +---+---+---+---+---+
| s2 | ptr --|----| h | e | l | l | o |
| | len=5 | +---+---+---+---+---+
| | cap=5 |
+------+---------+
There is still only one pointer to the heap data. When s2 goes out of scope,
the heap memory is freed exactly once.
Compare to C, where this "just works" but is dangerous:
/* ownership_c.c -- C has no concept of ownership */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char *s1 = malloc(6);
strcpy(s1, "hello");
char *s2 = s1; /* both pointers to same memory */
free(s1);
/* s2 is now dangling -- use-after-free if accessed */
/* free(s2); */ /* double-free if called */
return 0;
}
Rust Note: C trusts the programmer to track who "owns" each allocation mentally. Rust makes ownership explicit in the type system and enforces it at compile time. There is zero runtime cost.
Move Semantics
Passing a value to a function moves it. The caller loses access.
// move_fn.rs fn take_ownership(s: String) { println!("got: {}", s); } // s is dropped here fn main() { let s = String::from("hello"); take_ownership(s); // println!("{}", s); // ERROR: value moved }
To let the function use the value without taking ownership, pass a reference (borrowing, covered in Chapter 7).
// borrow_fn.rs fn borrow(s: &String) { println!("borrowed: {}", s); } // nothing dropped -- we only had a reference fn main() { let s = String::from("hello"); borrow(&s); println!("still mine: {}", s); // OK }
Copy vs Clone
Copy Types
Simple, stack-only types implement the Copy trait. Assignment copies the bits
instead of moving.
// copy_demo.rs fn main() { let x: i32 = 42; let y = x; // copy, not move -- i32 implements Copy println!("x = {}, y = {}", x, y); // both valid let a: f64 = 3.14; let b = a; // copy println!("a = {}, b = {}", a, b); // both valid }
Types that implement Copy: all integer types, f32, f64, bool, char,
tuples of Copy types, fixed-size arrays of Copy types.
Types that do NOT implement Copy: String, Vec<T>, Box<T>, anything
that owns heap memory.
Clone
Clone is explicit duplication. You call .clone() to make a deep copy.
// clone_demo.rs fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); // deep copy: new heap allocation println!("s1 = {}", s1); // OK -- s1 still valid println!("s2 = {}", s2); }
After clone:
Stack Heap
+------+---------+ +---+---+---+---+---+
| s1 | ptr --|----| h | e | l | l | o | <-- allocation 1
| | len=5 | +---+---+---+---+---+
+------+---------+
+------+---------+ +---+---+---+---+---+
| s2 | ptr --|----| h | e | l | l | o | <-- allocation 2
| | len=5 | +---+---+---+---+---+
+------+---------+
Two separate heap allocations. No aliasing.
Try It: Try to assign a
Vec<i32>without cloning. Observe the move error. Then add.clone()and see that both vectors work independently.
Lifetimes: What 'a Means
A lifetime is the scope during which a reference is valid. Usually, the compiler infers lifetimes automatically. When it cannot, you annotate them.
// lifetime_basic.rs fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1.len() >= s2.len() { s1 } else { s2 } } fn main() { let s1 = String::from("long string"); let result; { let s2 = String::from("hi"); result = longer(&s1, &s2); println!("longer: {}", result); } // println!("{}", result); // Would fail if s2's data were used }
The 'a annotation says: "the returned reference lives at least as long as
the shorter of the two input references." This lets the compiler verify that
the returned reference does not outlive its data.
Lifetime visualization:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str
^^ ^^ ^^
| | |
+------- all the same 'a -----+
|
The returned reference is valid for
the INTERSECTION of s1 and s2's lifetimes.
Timeline:
|---- s1 valid -----------------------------|
| |---- s2 valid ----| |
| |---- 'a ---------| |
| |-- result valid --| |
Why the Compiler Needs Lifetimes
Without lifetimes, the compiler cannot tell if this function is safe:
// lifetime_needed.rs -- what if there were no annotations? fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &byte) in bytes.iter().enumerate() { if byte == b' ' { return &s[0..i]; } } s } fn main() { let sentence = String::from("hello world"); let word = first_word(&sentence); println!("first word: {}", word); }
This compiles without explicit lifetime annotations because of lifetime elision rules. The compiler infers that the output lifetime matches the input. Here are the three elision rules:
- Each reference parameter gets its own lifetime.
- If there is exactly one input lifetime, it is assigned to all output lifetimes.
- If one parameter is
&selfor&mut self, its lifetime is assigned to outputs.
When these rules are insufficient, you must annotate manually.
Structs with References
If a struct holds a reference, it needs a lifetime parameter.
// struct_lifetime.rs struct Excerpt<'a> { text: &'a str, } impl<'a> Excerpt<'a> { fn new(text: &'a str) -> Self { Excerpt { text } } fn display(&self) { println!("Excerpt: {}", self.text); } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let excerpt = Excerpt::new(&novel[..16]); excerpt.display(); }
$ rustc struct_lifetime.rs && ./struct_lifetime
Excerpt: Call me Ishmael.
The lifetime 'a guarantees that the Excerpt cannot outlive the string it
references. In C, you would just store a const char * with no such guarantee.
/* struct_lifetime.c -- C version: no safety */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct excerpt {
const char *text; /* dangling pointer? who knows */
};
int main(void)
{
struct excerpt e;
{
char *novel = strdup("Call me Ishmael. Some years ago...");
e.text = novel;
free(novel); /* e.text is now dangling */
}
/* printf("%s\n", e.text); */ /* undefined behavior */
return 0;
}
Caution: The C version compiles without warnings. The dangling pointer is invisible to the compiler. Rust rejects the equivalent code outright.
The Borrow Checker in Action
The borrow checker enforces ownership and lifetime rules at compile time. Here is a classic example it catches:
// borrow_checker.rs -- This will NOT compile fn main() { let mut v = vec![1, 2, 3]; let first = &v[0]; // immutable borrow v.push(4); // mutable borrow (push might reallocate!) println!("{}", first); // ERROR: first might be dangling }
error[E0502]: cannot borrow `v` as mutable because it is also
borrowed as immutable
Why? push might reallocate the underlying buffer, invalidating first.
The borrow checker catches this at compile time. In C, this is a silent bug:
/* borrow_bug.c -- C version: silent bug */
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *arr = malloc(3 * sizeof(int));
arr[0] = 1; arr[1] = 2; arr[2] = 3;
int *first = &arr[0]; /* pointer into arr */
/* realloc might move the buffer */
int *tmp = realloc(arr, 100 * sizeof(int));
if (tmp) arr = tmp;
/* first might point to freed memory now */
printf("%d\n", *first); /* undefined behavior */
free(arr);
return 0;
}
Rc<T>: Reference-Counted Shared Ownership
Sometimes multiple parts of your program need to own the same data. Rc<T>
(Reference Counted) tracks how many owners exist and frees the data when the
count reaches zero.
// rc_demo.rs use std::rc::Rc; fn main() { let a = Rc::new(String::from("shared data")); println!("ref count after a: {}", Rc::strong_count(&a)); let b = Rc::clone(&a); // increment ref count, not deep copy println!("ref count after b: {}", Rc::strong_count(&a)); { let c = Rc::clone(&a); println!("ref count after c: {}", Rc::strong_count(&a)); } // c dropped, ref count decremented println!("ref count after c dropped: {}", Rc::strong_count(&a)); println!("data: {}", a); }
$ rustc rc_demo.rs && ./rc_demo
ref count after a: 1
ref count after b: 2
ref count after c: 3
ref count after c dropped: 2
data: shared data
Rc layout:
Stack Heap (Rc control block + data)
+---+---------+ +----------------+-------------------+
| a | ptr --|----->| strong_count=2 | "shared data" |
+---+---------+ | weak_count=0 | |
+---+---------+ +----------------+-------------------+
| b | ptr --|------^
+---+---------+
When all Rc's drop, strong_count hits 0 -> data freed.
Caution:
Rc<T>is single-threaded only. It does not use atomic operations. Using it across threads is a compile error.
Arc<T>: Atomic Reference Counting
Arc<T> is the thread-safe version of Rc<T>. It uses atomic operations to
update the reference count.
// arc_demo.rs use std::sync::Arc; use std::thread; fn main() { let data = Arc::new(vec![1, 2, 3, 4, 5]); let mut handles = vec![]; for i in 0..3 { let data_clone = Arc::clone(&data); let handle = thread::spawn(move || { let sum: i32 = data_clone.iter().sum(); println!("thread {}: sum = {}", i, sum); }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("ref count: {}", Arc::strong_count(&data)); }
$ rustc arc_demo.rs && ./arc_demo
thread 0: sum = 15
thread 1: sum = 15
thread 2: sum = 15
ref count: 1
Driver Prep: In Rust-for-Linux,
Arc<T>is used for shared device state. The kernel's ownstruct krefis the C equivalent -- a manual reference counter with explicitkref_getandkref_putcalls.
When to Use What
+------------------+--------------------------------------------+
| Ownership Model | Use When |
+------------------+--------------------------------------------+
| T (owned) | Single owner, value moves with assignment |
| &T | Read-only access, no ownership change |
| &mut T | Exclusive read-write access, temporary |
| Box<T> | Single owner, heap allocation needed |
| Rc<T> | Multiple owners, single-threaded |
| Arc<T> | Multiple owners, multi-threaded |
| Rc<RefCell<T>> | Multiple owners + interior mutability (ST) |
| Arc<Mutex<T>> | Multiple owners + interior mutability (MT) |
+------------------+--------------------------------------------+
'static Lifetime
The 'static lifetime means the reference is valid for the entire program
duration. String literals have 'static lifetime because they are embedded
in the binary.
// static_lifetime.rs fn get_greeting() -> &'static str { "Hello, world!" // string literal: lives forever } fn main() { let s = get_greeting(); println!("{}", s); }
'static does NOT mean "allocated forever." It means "valid for the rest of
the program." Leaked memory is also 'static, but that is usually a bug.
When to Use unsafe
unsafe does not turn off the borrow checker. It unlocks five specific powers:
- Dereference raw pointers (
*const T,*mut T) - Call
unsafefunctions - Access mutable statics
- Implement
unsafetraits - Access fields of
uniontypes
// unsafe_demo.rs fn main() { let mut x = 42; // Creating raw pointers is safe let r1 = &x as *const i32; let r2 = &mut x as *mut i32; // Dereferencing raw pointers requires unsafe unsafe { println!("r1 = {}", *r1); *r2 = 99; println!("r2 = {}", *r2); } // Calling C functions via FFI unsafe { let pid = libc_getpid(); println!("pid = {}", pid); } } // Declaring an external C function extern "C" { #[link_name = "getpid"] fn libc_getpid() -> i32; }
$ rustc unsafe_demo.rs && ./unsafe_demo
r1 = 42
r2 = 99
pid = 12345
Driver Prep: Rust-for-Linux kernel modules use
unsafeat the boundary between Rust and the C kernel API. The goal is to wrap unsafe operations in safe abstractions so that driver authors rarely needunsafein their own code.
C Trusts the Programmer, Rust Trusts the Compiler
This is the philosophical divide. C gives you full control and assumes you know what you are doing. Rust restricts what you can express and proves correctness at compile time.
/* trust_programmer.c */
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(sizeof(int));
*p = 42;
int *alias = p; /* two pointers, same memory */
free(p); /* free through one */
*alias = 99; /* use through other -- UB */
/* C: compiles, runs, "works" until it doesn't */
return 0;
}
// trust_compiler.rs fn main() { let p = Box::new(42); // let alias = p; // ownership moves, p is invalidated // println!("{}", *p); // compile error: use of moved value // Rust: does not compile. Bug caught before it exists. }
The cost of Rust's approach: a learning curve and occasional fights with the borrow checker. The benefit: entire categories of bugs are impossible.
Knowledge Check
- What is the difference between
CopyandClone? - What does the lifetime
'ainfn foo<'a>(x: &'a str) -> &'a strmean? - Why is
Rc<T>not safe to use across threads?
Common Pitfalls
- Cloning everything to avoid the borrow checker -- this works but defeats the purpose. Restructure your code instead.
- Confusing
movein closures with ownership transfer --movecaptures variables by value, taking ownership. - Forgetting that
Rccycles leak -- useWeak<T>references to break cycles. - Overusing
'static-- not every reference needs to live forever. Use the narrowest lifetime that works. - Putting lifetimes on everything -- trust the elision rules first. Annotate only when the compiler asks.
- Using
unsafeto bypass the borrow checker -- if the borrow checker rejects your code, you almost certainly have a real bug.unsafedoes not fix logic errors. - Thinking
unsafemeans "no rules" -- you still must uphold Rust's safety invariants.unsafemeans "I, the programmer, guarantee these invariants hold."