Rust Ownership | Ownership, Borrowing, and Lifetimes
이 글의 핵심
A practical guide to Rust ownership: rules for moves and borrows, slices, lifetimes, and how the borrow checker keeps memory safe without a GC.
Introduction
Ownership is Rust’s signature feature: memory safety without a garbage collector by enforcing rules at compile time. Think of each heap value as a single key to an apartment—only one variable holds the key; when it goes out of scope, Rust locks the door (drops) exactly once. There is no duplicate key that could double-free. Compare with C++ smart pointers and Python’s GC-backed objects.
1. Ownership rules
Rule 1: Every value has an owner
fn main() {
let s = String::from("hello");
// `s` owns the heap-allocated string
}
What this means: After String::from allocates on the heap, s is the sole owner of that memory. When s leaves scope, Rust calls drop—no manual free like in C.
Rule 2: Only one owner
Each value has exactly one owner at a time:
fn main() {
let s1 = String::from("hello");
// Move: ownership transfers fully to s2
// s1 is no longer valid
let s2 = s1;
// println!("{}", s1); // Compile error!
// "value borrowed here after move"
println!("{}", s2); // OK — s2 owns the data
}
// When s2 goes out of scope, the string is dropped once
Why move?
C++ pitfall:
// Shallow copy can lead to double free if not careful
std::string s1 = "hello";
std::string s2 = s1; // Both may share resources depending on implementation
// Destructors can run twice on the same resource if misdesigned
Perspective: In C++, if you do not consistently use value semantics, it is easy to get copy/move/destructor rules wrong. Rust defaults to move and makes expensive copies explicit with clone().
Rust’s approach:
- Deep copies are costly (allocation + memcpy)
- By default, ownership moves (transfer + invalidate source)
- Double free is impossible (single owner)
- Need a duplicate? Call
clone()explicitly
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy (explicit)
println!("{}, {}", s1, s2); // Both OK
clone in production: It can be CPU- and memory-heavy—profile hot loops and avoid redundant clones. Sometimes you choose clone() anyway to simplify APIs when correctness matters more than micro-optimization.
The Copy trait:
// Small stack types (integers, bool, etc.) implement Copy
// Assignment copies instead of moving
let x = 5;
let y = x; // Copy, not move
println!("{}, {}", x, y); // Both OK
// Cheap to copy: fixed small size on the stack
Rule 3: Drop at end of scope
fn main() {
{
let s = String::from("hello");
println!("{}", s);
} // `drop` runs for `s` here
// println!("{}", s); // Error: s is gone
}
RAII: Use the same pattern for resources (files, locks, connections) scoped to a block—cleanup runs automatically. This ties into the Drop trait and idiomatic resource management in Rust.
2. Functions and ownership
Moving into a function
fn main() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // Error: ownership moved
}
fn takes_ownership(s: String) {
println!("{}", s);
} // `s` is dropped here
Passing by value: Giving a String to a function consumes it for the caller. Use .clone() or references (&String / &str) if the caller must keep using it. Many codebases distinguish consuming APIs from borrowing APIs by naming and signatures.
Returning ownership
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
println!("{}, {}", s1, s3);
}
fn gives_ownership() -> String {
String::from("hello")
}
fn takes_and_gives_back(s: String) -> String {
s
}
3. References and borrowing
Immutable references (&)
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{} length: {}", s1, len); // s1 still usable
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s is a reference; nothing is dropped here
Mutable references (&mut)
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // hello, world
}
fn change(s: &mut String) {
s.push_str(", world");
}
The borrowing rules
Rust’s rules prevent data races at compile time:
fn main() {
let mut s = String::from("hello");
// Multiple immutable borrows are allowed
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2); // OK
// Only one mutable borrow at a time
let r3 = &mut s;
// let r4 = &mut s; // Error!
println!("{}", r3); // OK
// Cannot mix & and &mut in the same live region
let r5 = &s;
// let r6 = &mut s; // Error!
println!("{}", r5);
}
4. Slices
A slice references a contiguous portion of a collection:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello" as &str
let world = &s[6..11]; // "world"
println!("{}, {}", hello, world);
let hello2 = &s[..5];
let world2 = &s[6..];
let full = &s[..];
}
// First word: slice example
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let sentence = String::from("hello world");
let word = first_word(&sentence);
println!("first word: {}", word);
// If `word` borrows `sentence`, mutating `sentence` can fail to compile:
// let mut s = String::from("hello world");
// let word = first_word(&s);
// s.clear(); // Error while `word` is live
}
Slice types:
let s: String = String::from("hello");
let slice: &str = &s[0..2]; // "he"
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..3]; // [2, 3]
// Slice = pointer + length; out-of-range access panics
5. Lifetimes
Lifetime annotations
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("long string");
let s2 = String::from("short");
let result = longest(&s1, &s2);
println!("longest: {}", result);
}
Struct lifetimes
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("{}", excerpt.part);
}
6. Hands-on example
String processing
fn main() {
let text = String::from("hello rust world");
let words = split_words(&text);
println!("words: {:?}", words);
let first = first_word(&text);
println!("first word: {}", first);
}
fn split_words(s: &String) -> Vec<&str> {
s.split_whitespace().collect()
}
fn first_word(s: &String) -> &str {
s.split_whitespace().next().unwrap_or("")
}
Summary
Takeaways
- Ownership: one owner per value
- Move: transfer ownership
- Borrowing: use references (
&,&mut) - Lifetimes: how long references stay valid
- Safety: enforced at compile time
Next steps
- Structs and enums
- Error handling
- Traits
Compared to other languages
- C++ move semantics | Rvalue references and std::move
Related posts
- C++ vs Rust: ownership, safety, errors, concurrency, performance
- Rust memory safety deep dive | Borrow checker and lifetimes
- Rust vs C++ memory safety | Compiler errors [#47-3]
- C++ observer pointer
- C++ and Rust interoperability [#44-2]