본문으로 건너뛰기 Rust Ownership Debugging Case Study

Rust Ownership Debugging Case Study

Rust Ownership Debugging Case Study

이 글의 핵심

Solve real Rust ownership, borrowing, and lifetime errors beginners hit: reading borrow checker messages, RefCell, Rc, Arc, and multithreaded patterns—without fighting the compiler.

Introduction

“It works in C++—why not in Rust?” is something you hear a lot when learning Rust. This article uses real ownership errors to show how to understand the borrow checker and fix them.

What you will learn

  • How to read borrow checker error messages
  • How ownership, borrowing, and lifetimes play out in practice
  • Patterns with RefCell, Rc, Arc, and more
  • How to work with Rust’s memory safety guarantees

Table of contents

  1. Case 1: “cannot move out of borrowed content”
  2. Case 2: “cannot borrow as mutable more than once”
  3. Case 3: “lifetime may not live long enough”
  4. Case 4: “cannot return reference to local variable”
  5. Case 5: Shared state across threads
  6. Pattern cheat sheet: what to use when
  7. Conclusion

1. Case 1: “cannot move out of borrowed content”

Problem code

struct User {
    name: String,
    email: String,
}

fn process_users(users: &Vec<User>) {
    for user in users {
        let name = user.name; // ❌ cannot move out of `user.name`
        println!("Processing: {}", name);
    }
}

Error message

error[E0507]: cannot move out of `user.name` which is behind a shared reference
  --> src/main.rs:8:20
   |
8  |         let name = user.name;
   |                    ^^^^^^^^^ move occurs because `user.name` has type `String`, which does not implement the `Copy` trait

Why is this an error?

  • users is an immutable borrow (&Vec<User>)
  • user.name is a String (owned type)
  • let name = user.name tries to move ownership out
  • You cannot move out of borrowed data!

Fixes

// Option 1: use references only
fn process_users(users: &Vec<User>) {
    for user in users {
        let name = &user.name; // ✅ borrow
        println!("Processing: {}", name);
    }
}

// Option 2: clone
fn process_users(users: &Vec<User>) {
    for user in users {
        let name = user.name.clone(); // ✅ copy the string
        println!("Processing: {}", name);
    }
}

// Option 3: take ownership
fn process_users(users: Vec<User>) { // remove &
    for user in users {
        let name = user.name; // ✅ move is allowed
        println!("Processing: {}", name);
    }
}

2. Case 2: “cannot borrow as mutable more than once”

Problem code

struct ChatRoom {
    users: Vec<User>,
    messages: Vec<String>,
}

impl ChatRoom {
    fn broadcast(&mut self, sender: &User) {
        let msg = format!("{}: Hello", sender.name);
        
        // ❌ cannot borrow `self` as mutable more than once
        for user in &mut self.users {
            self.messages.push(msg.clone()); // 💥 second mutable borrow!
        }
    }
}

Error message

error[E0499]: cannot borrow `self.messages` as mutable more than once at a time
  --> src/main.rs:12:13
   |
10 |         for user in &mut self.users {
   |                     --------------- first mutable borrow occurs here
11 |             self.messages.push(msg.clone());
   |             ^^^^^^^^^^^^^ second mutable borrow occurs here

Why is this an error?

  • &mut self.users is the first mutable borrow
  • self.messages.push() tries a second mutable borrow
  • Rust allows only one mutable reference at a time

Fixes

Here is the broadcast implementation:

// Option 1: split borrows across fields
impl ChatRoom {
    fn broadcast(&mut self, sender: &User) {
        let msg = format!("{}: Hello", sender.name);
        
        let users = &mut self.users;
        let messages = &mut self.messages;
        
        for user in users {
            messages.push(msg.clone()); // ✅ different fields
        }
    }
}

// Option 2: push the message before the loop
impl ChatRoom {
    fn broadcast(&mut self, sender: &User) {
        let msg = format!("{}: Hello", sender.name);
        self.messages.push(msg.clone()); // add first
        
        for user in &mut self.users {
            // no longer borrowing messages here
            user.notify();
        }
    }
}

// Option 3: use indices
impl ChatRoom {
    fn broadcast(&mut self, sender: &User) {
        let msg = format!("{}: Hello", sender.name);
        
        for i in 0..self.users.len() {
            self.messages.push(msg.clone()); // ✅ indexing is not a borrow conflict
        }
    }
}

3. Case 3: “lifetime may not live long enough”

Problem code

struct UserCache {
    users: Vec<User>,
}

impl UserCache {
    fn find(&self, name: &str) -> Option<&User> {
        self.users.iter().find(|u| u.name == name)
    }
    
    fn get_or_create(&mut self, name: &str) -> &User {
        // ❌ lifetime error
        if let Some(user) = self.find(name) {
            return user; // 💥 borrow active but &mut self needed below
        }
        
        self.users.push(User { name: name.to_string(), email: String::new() });
        self.users.last().unwrap()
    }
}

Error message

error[E0502]: cannot borrow `self.users` as mutable because it is also borrowed as immutable
  --> src/main.rs:15:9
   |
12 |         if let Some(user) = self.find(name) {
   |                             ---- immutable borrow occurs here
13 |             return user;
   |                    ---- returning this value requires that `*self` is borrowed for `'1`
...
15 |         self.users.push(...);
   |         ^^^^^^^^^^^ mutable borrow occurs here

Fixes

// Option 1: use an index
impl UserCache {
    fn get_or_create(&mut self, name: &str) -> &User {
        if let Some(idx) = self.users.iter().position(|u| u.name == name) {
            return &self.users[idx]; // ✅ fresh borrow
        }
        
        self.users.push(User { name: name.to_string(), email: String::new() });
        self.users.last().unwrap()
    }
}

// Option 2: Entry API (HashMap)
use std::collections::HashMap;

struct UserCache {
    users: HashMap<String, User>,
}

impl UserCache {
    fn get_or_create(&mut self, name: &str) -> &User {
        self.users.entry(name.to_string())
            .or_insert_with(|| User { name: name.to_string(), email: String::new() })
    }
}

4. Case 4: “cannot return reference to local variable”

Problem code

fn get_default_user() -> &User {
    let user = User {
        name: "Guest".to_string(),
        email: "guest@example.com".to_string(),
    };
    &user // ❌ `user` is dropped at the end of the function
}

Error message

error[E0515]: cannot return reference to local variable `user`
  --> src/main.rs:8:5
   |
8  |     &user
   |     ^^^^^ returns a reference to data owned by the current function

Fixes

// Option 1: return ownership
fn get_default_user() -> User {
    User {
        name: "Guest".to_string(),
        email: "guest@example.com".to_string(),
    }
}

// Option 2: 'static lifetime (illustrative; String cannot be const here)
fn get_default_user() -> &'static User {
    static DEFAULT: User = User {
        name: String::new(), // ❌ not valid in const—example only
    };
    &DEFAULT
}

// Option 2b: lazy_static
use lazy_static::lazy_static;

lazy_static! {
    static ref DEFAULT_USER: User = User {
        name: "Guest".to_string(),
        email: "guest@example.com".to_string(),
    };
}

fn get_default_user() -> &'static User {
    &DEFAULT_USER
}

// Option 3: heap allocation with Box
fn get_default_user() -> Box<User> {
    Box::new(User {
        name: "Guest".to_string(),
        email: "guest@example.com".to_string(),
    })
}

5. Case 5: Shared state across threads

Problem code

use std::thread;

struct Counter {
    count: i32,
}

fn main() {
    let counter = Counter { count: 0 };
    
    let handle = thread::spawn(|| {
        counter.count += 1; // ❌ closure may outlive the current function
    });
    
    handle.join().unwrap();
}

Error message

error[E0373]: closure may outlive the current function, but it borrows `counter`, which is owned by the current function
  --> src/main.rs:9:31
   |
9  |     let handle = thread::spawn(|| {
   |                                ^^ may outlive borrowed value `counter`
10 |         counter.count += 1;
   |         ------- `counter` is borrowed here

Fixes

// Option 1: Arc + Mutex (thread-safe sharing)
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let counter_clone = Arc::clone(&counter);
    
    let handle = thread::spawn(move || {
        let mut count = counter_clone.lock().unwrap();
        *count += 1;
    });
    
    handle.join().unwrap();
    println!("Count: {}", *counter.lock().unwrap());
}

// Option 2: channels (message passing)
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
    
    thread::spawn(move || {
        tx.send(1).unwrap();
    });
    
    let count = rx.recv().unwrap();
    println!("Count: {}", count);
}

// Option 3: atomic type (AtomicI32)
use std::sync::atomic::{AtomicI32, Ordering};

fn main() {
    let counter = Arc::new(AtomicI32::new(0));
    let counter_clone = Arc::clone(&counter);
    
    let handle = thread::spawn(move || {
        counter_clone.fetch_add(1, Ordering::SeqCst);
    });
    
    handle.join().unwrap();
    println!("Count: {}", counter.load(Ordering::SeqCst));
}

6. Pattern cheat sheet: what to use when

Ownership vs borrowing

SituationApproachExample
Read-onlyImmutable reference &Tfn print(user: &User)
Mutation neededMutable reference &mut Tfn update(user: &mut User)
Need ownershipValue Tfn consume(user: User)
Copy typesCopy traitfn calc(x: i32)

Interior mutability

TypeUse caseThread-safe
Cell<T>Interior mutability for Copy typesNo
RefCell<T>Runtime borrow checksNo
Mutex<T>Shared mutation across threadsYes
RwLock<T>Read-heavy workloadsYes

Shared ownership

TypeUse caseThread-safe
Rc<T>Single-threaded sharingNo
Arc<T>Multi-threaded sharingYes
Weak<T>Break reference cyclesDepends

Common combinations

The following example demonstrates the concept in rust:

// Single-threaded: Rc + RefCell
use std::rc::Rc;
use std::cell::RefCell;

let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone = Rc::clone(&shared);

shared.borrow_mut().push(4);
println!("{:?}", clone.borrow()); // [1, 2, 3, 4]

// Multi-threaded: Arc + Mutex
use std::sync::{Arc, Mutex};

let shared = Arc::new(Mutex::new(vec![1, 2, 3]));
let clone = Arc::clone(&shared);

thread::spawn(move || {
    clone.lock().unwrap().push(4);
});

7. Hands-on example: event system

C++-style (does not compile)

struct EventManager {
    listeners: Vec<Box<dyn Fn(&Event)>>,
}

impl EventManager {
    fn subscribe(&mut self, callback: impl Fn(&Event) + 'static) {
        self.listeners.push(Box::new(callback));
    }
    
    fn publish(&self, event: &Event) {
        for listener in &self.listeners {
            listener(event); // ✅ this part is fine
        }
    }
}

// Problem: what if a subscriber wants to mutate the EventManager?
fn main() {
    let mut mgr = EventManager { listeners: vec![] };
    
    mgr.subscribe(|event| {
        mgr.publish(event); // ❌ cannot borrow `mgr` as mutable
    });
}

Rust-style (Rc + RefCell)

use std::rc::Rc;
use std::cell::RefCell;

struct EventManager {
    listeners: Vec<Box<dyn Fn(&Event)>>,
}

fn main() {
    let mgr = Rc::new(RefCell::new(EventManager { listeners: vec![] }));
    let mgr_clone = Rc::clone(&mgr);
    
    mgr.borrow_mut().subscribe(Box::new(move |event| {
        // use mgr_clone inside the closure
        mgr_clone.borrow().publish(event);
    }));
}

8. Debugging tips

How to read the error message

error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
  --> src/main.rs:10:5
   |
8  |     let r = &x;
   |             -- immutable borrow occurs here
9  |     
10 |     x.push(1);
   |     ^^^^^^^^^ mutable borrow occurs here
11 |     println!("{}", r);
   |                    - immutable borrow later used here

Reading order:

  1. Error code: E0502 (conflicting borrows)
  2. Where it breaks: line 10
  3. Cause: immutable borrow on line 8
  4. Where it is still used: through line 11

rustc —explain

$ rustc --explain E0502

This error indicates that you are trying to borrow a value as mutable when it
is already borrowed as immutable.
...

Clippy

$ cargo clippy

warning: this `RefCell` Ref is held across an 'await' point
  --> src/main.rs:10:9
   |
10 |     let data = cell.borrow();
   |         ^^^^
   |
   = help: consider using a `Mutex` instead

Conclusion

The borrow checker feels restrictive at first, but it guarantees memory safety at compile time. With these cases you have:

  1. Learned to read error messages carefully
  2. Understood how ownership, borrowing, and lifetimes interact
  3. Picked up RefCell, Rc, Arc, and related patterns
  4. Started thinking in a way that differs from C++

Takeaway: Do not fight the borrow checker—learn Rust’s rules and lean into them.


FAQ

Q1. Is Rust harder than C++?

The early learning curve is steep, but once you are comfortable you can prevent entire classes of memory bugs.

Q2. Should I avoid overusing RefCell?

RefCell checks borrows at runtime and can panic. Prefer compile-time borrowing when possible.

Q3. Does Arc<Mutex<T>> hurt performance?

Yes, there is overhead. Use it only where needed; if reads dominate, consider RwLock.



Practical checklist

Borrow checker troubleshooting

  • Read the full error message
  • Trace borrow scopes (where they start and end)
  • Decide whether you need ownership or only a reference
  • Separate mutable vs immutable borrows
  • Check lifetime relationships
  • Pick the right pattern (Rc, RefCell, Arc, Mutex)
  • Run Clippy for extra hints

Multi-threaded safety

  • Verify Send / Sync
  • Consider Arc + Mutex
  • Watch for deadlocks
  • Consider channels
  • Use atomics when appropriate

Keywords

Rust, Ownership, Borrow Checker, Borrowing, Lifetime, RefCell, Rc, Arc, Mutex, Case Study, Debugging, Error Resolution


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Rust, Ownership, Borrow Checker, Borrowing, Lifetime, Case Study, Debugging.