Rust Ownership | Ownership, Borrowing, and Lifetimes

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

  1. Ownership: one owner per value
  2. Move: transfer ownership
  3. Borrowing: use references (&, &mut)
  4. Lifetimes: how long references stay valid
  5. 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

  • 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]