TT Lab
Get started
Learn Learning paths Courses

Rust — What the Compiler Refuses

Why You Cannot Use It Twice

Continue in TT Lab

In one line

Rust enforces a single owner for every value. Once you hand a value to someone else, you can no longer use it at the original place. That one rule gives you memory safety without a GC.

Why this was needed

There are three classic incidents in the C family.

All three come from it being unclear "who owns this value." A GC eliminates the first two, but it costs pause time and memory, and the third one still remains.

Rust chose a different answer: it tracks the owner at compile time. The run-time cost is zero.

There are only three rules

  1. Every value has exactly one owner
  2. When the owner goes away, the value goes away too
  3. When you hand over (move) a value somewhere else, the original owner loses it
let s = String::from("hi");
let t = s;              // 주인이 t 로 옮겨졌다
println!("{}", s);      // ❌ 컴파일 오류: s 는 이미 넘겨졌다

The error message says exactly that.

error[E0382]: borrow of moved value: `s`
  = note: move occurs because `s` has type `String`,
          which does not implement the `Copy` trait

Types that implement Copy are copied. These are things like integers, booleans, and characters. That is why let a = 5; let b = a; is fine: they have a fixed size and do not use the heap.

Borrowing

To use a value briefly without handing it over, you borrow it.

fn len(s: &String) -> usize { s.len() }   // 읽기만 빌림
let s = String::from("hi");
println!("{} {}", len(&s), s);            // ✅ s 는 여전히 내 것

Borrowing has one more rule.

Many read borrows can exist at the same time, but there can be only one write borrow, and while it exists you cannot read either.

let mut v = vec![1, 2, 3];
let first = &v[0];      // 읽기 빌림
v.push(4);              // ❌ 쓰기 빌림이 필요한데 읽기가 살아 있다
println!("{}", first);

What this prevents is a real incident. If push reallocates the vector, the address that first was pointing to becomes invalid. In C++ this passes silently, and later you read a strange value.

Lifetimes

A borrow cannot outlive the original. The compiler checks that.

fn dangle() -> &String {          // ❌
    let s = String::from("hi");
    &s                            // s 는 함수가 끝나면 사라진다
}

In most cases lifetimes are inferred. Writing an explicit 'a is rarer than you might think, and it is mainly for telling the compiler which of several references the result is tied to.

Common misconceptions

"Just clone it." That works. And it is often the right answer. Rather than spending a whole day fighting ownership, it is better to clone() and move on. If performance becomes a problem, you look at a profiler then and fix it. Trying to optimize with references from the start is the most common reason Rust feels hard.

"unsafe is bad." unsafe is a marker that says "the compiler cannot check this here, so I guarantee it." There is plenty of it inside the standard library. What is bad is not using unsafe, but not writing down why it is safe.

A toolbox for ownership problems

Instead of fighting the borrow checker, pick the tool that fits the situation and most problems go away.

Situation Tool Cost
Only reading briefly &T None
Modifying briefly &mut T No other references in the meantime
Handing over ownership The value itself You cannot use the original variable
Copying is cheap .clone() Cost of copying
Several places own it together Rc<T> (single thread) Reference count, cycle leaks
Several threads share it Arc<T> Atomic count (slightly slower)
Sharing and also modifying Arc<Mutex<T>> Lock cost, possible deadlock

It is not wrong for a beginner to use .clone(). First make it work, and when the profiler points at that copy, switch to a reference then. That is far better than giving up after fighting lifetime annotations from the start.

The order for reading a compile error

Rust's error messages are long, but their structure is consistent.

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let first = &v[0];        ← 여기서 불변으로 빌렸고
  |                  - immutable borrow occurs here
4 |     v.push(4);                ← 여기서 가변으로 빌리려 한다
  |     ^^^^^^^^^ mutable borrow occurs here
5 |     println!("{}", first);    ← 불변 빌림이 여기까지 살아 있다
  |                    ----- immutable borrow later used here

Read the three arrows in order: where it was borrowed, where it conflicts, and why it is still alive. The last line is the key. If you do not use first, the borrow ends before push and the error goes away (NLL, non-lexical lifetimes).

With rustc --explain E0502 you can see the full explanation of that error. Each code comes with examples, so for an error you meet for the first time, reading this first is the quickest route.

When to use unsafe

Almost never. But unsafe does not mean "turn the checks off"; it means "I promise the compiler that I uphold the invariants." Only five additional operations are allowed (dereferencing a raw pointer, calling an unsafe function, and so on), and the rest of the checks stay as they are.

There are three places you meet it in practice: when calling a C library (FFI), when implementing a data structure yourself (a doubly linked list), and when it is a proven performance bottleneck.

Keep an unsafe block as narrow as possible, and cover it with a safe API. Then only that block needs human review, and users see only the safe interface. The standard library has exactly this structure.

What really matters in practice

The habit of reading error messages to the end is half of learning Rust. rustc usually even tells you how to fix it.

help: consider cloning the value if the performance cost is acceptable
   |
5  |     let t = s.clone();
   |              ++++++++

And cargo clippy points out places that compile but have a better way. While you are learning, keeping clippy turned on is like reading one more book.