Rust 12 🦀 Core Ownership Rules and Memory Safety Guarantee
Ownership is Rust’s most distinctive feature and the mechanism that makes memory safety possible without a garbage collector. Every value in a Rust program has exactly one owner, ownership can move but never duplicates, and when the owner goes out of scope, the value is dropped. These three rules sound simple, but they have profound consequences: they eliminate use-after-free, double-free, and memory leaks without runtime cost, and they form the foundation on which borrowing, lifetimes, and thread safety are built.
The rules exist because of the memory layout covered in the previous chapter. The heap needs a policy for deallocation; the stack does not. Ownership is that policy. When a String is created, the heap allocation has an owner, and when the owner goes out of scope, the allocation is freed. When ownership moves, the responsibility moves with it. When a value is borrowed, the owner retains responsibility, and the compiler verifies that the borrow does not outlive the owner. Every rule follows from the fact that the heap must be freed exactly once, at the right time, by the right code.
This chapter covers the three ownership rules, moves and copies, the Copy and Clone traits, scope and drop, ownership in functions, and the memory safety guarantees that ownership provides.
Key point: Every value has exactly one owner. Ownership moves on assignment, function call, and return, and the original variable becomes invalid. When the owner goes out of scope, the value is dropped. Types that implement Copy are copied instead of moved. Ownership eliminates use-after-free, double-free, and memory leaks without a garbage collector.
Why ownership exists
The deallocation problem. Every heap allocation must be freed exactly once. Free too early and you have a use-after-free. Free too late and you have a leak. Free twice and you corrupt the allocator. In C, the programmer is responsible. In Java and Go, a garbage collector handles it. In Rust, ownership handles it: the compiler tracks which variable owns each heap allocation and inserts the deallocation when the owner’s scope ends.
The aliasing problem. When two pointers refer to the same memory and one of them frees it, the other becomes dangling. When one of them writes while the other reads, the result is undefined. Ownership prevents aliasing by ensuring that at most one variable owns a value, and borrowing prevents simultaneous mutation and reading.
The concurrency problem. Data races occur when two threads access the same memory and at least one writes. Ownership extends to threads: a value can be sent to another thread (moving ownership) or shared immutably, but not shared mutably without synchronization. The compiler enforces this.
The performance problem. A garbage collector introduces pauses and runtime overhead. Ownership is checked at compile time, so the resulting binary has no runtime cost for memory management. The deallocation is inserted as a plain function call.
The reasoning problem. Ownership makes the flow of memory explicit. Reading a function signature tells you whether it takes ownership, borrows, or returns a new value. There is no hidden state, no reference counting to track, no uncertainty about who is responsible for freeing.
a. Rule one: each value has one owner
Every value in Rust has exactly one variable that owns it. The owner is the variable that is responsible for the value’s lifetime.
let s = String::from("hello");
// s is the owner of the String
The String value is not shared. It has one owner, s. When s goes out of scope, the String is dropped, and the heap allocation is freed.
Ownership is not a property of the type; it is a property of the binding. A String is always owned by some variable, and that variable is the one responsible for freeing the heap when it goes out of scope.
{
let s = String::from("hello");
// s owns the String
} // s goes out of scope, the String is freed
The stack part of s is popped automatically, and the compiler-inserted destructor frees the heap part. This is the entire mechanism: one owner, one drop.
b. Rule two: ownership moves
When a value is assigned to another variable, passed to a function, or returned from a function, ownership moves. The original variable becomes invalid.
let s1 = String::from("hello");
let s2 = s1;
// s1 is now invalid; s2 owns the String
The move copies the stack part (pointer, length, capacity) from s1 to s2, and s1 is marked as moved. The heap allocation stays where it is, with s2 as the new owner. The compiler rejects any use of s1 after the move.
// println!("{}", s1); // Error: borrow of moved value: `s1`
The error is a compile-time check, not a runtime failure. The compiler tracks the state of every variable and rejects code that uses a moved value.
Moves happen in three situations:
Assignment.
let s1 = String::from("hello");
let s2 = s1; // s1 moved to s2
Passing to a function.
fn takes_ownership(s: String) {
println!("{}", s);
}
let s = String::from("hello");
takes_ownership(s); // s moved into the function
// s is no longer valid
Returning from a function.
fn gives_ownership() -> String {
String::from("hello")
}
let s = gives_ownership(); // ownership moves to s
The rule is consistent: whenever a value is transferred, the original binding is invalidated. This is what makes Rust’s memory management sound, because there is never more than one owner.
c. Rule three: values are dropped when the owner goes out of scope
When the owner of a value goes out of scope, the value is dropped. For heap values, this means the heap allocation is freed.
{
let s = String::from("hello");
// s is valid here
} // s goes out of scope; the String is dropped
The drop happens at the closing brace of the scope. Rust inserts a call to the drop function, which frees the heap allocation. This is automatic and deterministic. There is no garbage collector, no finalizer, and no need to call free.
The Drop trait can be implemented for custom types to specify what happens when a value is dropped:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropping {}", self.name);
}
}
{
let r = Resource { name: String::from("file") };
} // prints "Dropping file"
The drop method is called automatically when the value goes out of scope. It cannot be called explicitly (that would double-drop), but std::mem::drop can be used to drop a value early:
let r = Resource { name: String::from("file") };
drop(r); // explicitly drops r
// r is no longer valid
std::mem::drop is a function that takes a value by value and lets it go out of scope, triggering its destructor. It is used when the drop should happen before the enclosing scope ends.
d. Copy versus move
Not all values move. Types that implement the Copy trait are copied instead of moved. A copy duplicates the bits, and both the original and the copy remain valid.
let x = 5;
let y = x;
println!("{} {}", x, y); // 5 5 — both valid
An i32 implements Copy, so let y = x copies the value, and x remains usable. The same is true for bool, f64, char, and tuples of Copy types.
let point = (1, 2);
let copied = point;
println!("{:?} {:?}", point, copied); // both valid
The Copy trait is implemented for types that can be duplicated with a simple bitwise copy and that have no resources to free. A String cannot be Copy because it owns a heap allocation, and copying it would require allocating new memory or creating an alias that would lead to a double-free.
// String does NOT implement Copy
let s1 = String::from("hello");
let s2 = s1; // moves, not copies
The distinction is enforced by the compiler. A type either implements Copy or it does not. If it does, assignment copies. If it does not, assignment moves.
| Type | Copy | Behavior on assignment |
|---|---|---|
i32, u64, f64 | Yes | Copy |
bool, char | Yes | Copy |
(i32, i32) | Yes | Copy |
[u8; 4] | Yes | Copy |
&T | Yes | Copy (the reference) |
String | No | Move |
Vec<T> | No | Move |
Box<T> | No | Move |
&mut T | No | Move |
A struct or enum is Copy only if all its fields are Copy. The derive macro can implement it:
#[derive(Copy, Clone)]
struct Point { x: i32, y: i32 }
A type cannot be Copy if it implements Drop, because dropping a copy would free the same resource twice.
e. Clone: explicit duplication
When a value should be duplicated rather than moved, clone is used. Cloning allocates new memory and copies the contents, so both the original and the clone are valid and independent.
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{} {}", s1, s2); // both valid
The difference between Copy and Clone is that Copy is implicit and cheap (a bitwise copy), while Clone is explicit and potentially expensive (it may allocate). Copy is for types with no heap resources; Clone is for types that need a deep copy.
A type can implement both. Copy requires Clone, so a Copy type is always cloneable, but cloning a Copy type is the same as copying it.
let x = 5;
let y = x.clone(); // works, but pointless for Copy types
The compiler warns about this, and the idiomatic code uses plain assignment for Copy types.
Cloning is the explicit way to say “I want two independent values.” Moves are the default; clones are chosen when the original must remain usable.
f. Ownership in functions
Function parameters and return values follow the same ownership rules as variables. A parameter that takes a value by value takes ownership. A return value transfers ownership to the caller.
fn take(s: String) {
println!("{}", s);
} // s is dropped here
fn give() -> String {
String::from("hello")
}
fn main() {
let s = give(); // ownership moves to s
take(s); // ownership moves into take
// s is no longer valid
}
Taking ownership in a function means the function is responsible for the value while it has it, and it drops the value when it returns unless it transfers ownership back.
Returning ownership from a function is the mechanism for transferring values out of a scope:
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
The format! macro creates a new String, and the function returns it. Ownership moves to the caller, who is now responsible for the value.
If a function needs to use a value without taking ownership, it takes a reference instead. References are covered in the next chapter, but the rule is simple: &T borrows without moving, and the owner retains responsibility.
fn length(s: &str) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let n = length(&s); // borrows, does not move
println!("{} has {} characters", s, n); // s is still valid
}
The distinction between taking ownership and borrowing is the most important decision in function design. Taking ownership means the function can do anything with the value, including storing it or returning it. Borrowing means the function can only use the value while the borrow is active, and the owner retains control.
g. Memory safety guarantees
Ownership provides three guarantees that are verified at compile time.
No use-after-free. A value cannot be used after it is dropped, because the drop happens when the owner goes out of scope, and any attempt to use the value after that is a compile error. There is no way to hold a reference to a dropped value without the compiler rejecting it.
No double-free. A value has exactly one owner, so it is dropped exactly once. Moving the value transfers ownership, and the original binding can no longer drop it. Cloning creates a new value with its own lifetime, so each clone is dropped independently.
No memory leaks. The compiler inserts a drop for every value that owns a heap allocation, and the drop happens at the end of the owner’s scope. There is no way to forget to free a value, and there is no garbage collector that might fail to run. (Leaks are still possible with std::mem::forget or reference cycles involving Rc and RefCell, but these are explicit choices rather than accidents.)
These guarantees hold without runtime cost. The compiler inserts the drop calls and verifies the ownership rules statically. There is no reference counting, no tracing, and no pauses. The resulting binary does exactly what it would do if a careful C programmer had written the frees by hand, but with the compiler proving that no mistake was made.
The guarantees extend to threads. A value can be sent to another thread by moving it, which transfers ownership, so only one thread has access at a time. Sharing a value across threads requires either immutable references (safe because nothing can write) or synchronization primitives like Mutex (which enforce exclusive access at runtime). The compiler enforces this through the Send and Sync marker traits.
Complete Example Session
// ============================================
// PART 1: OWNERSHIP BASICS
// ============================================
fn main() {
let s = String::from("hello");
// s owns the String
println!("{}", s);
} // s goes out of scope; the String is freed
// ============================================
// PART 2: MOVE ON ASSIGNMENT
// ============================================
fn main() {
let s1 = String::from("hello");
let s2 = s1;
// s1 is no longer valid
println!("{}", s2);
// println!("{}", s1); // Error: borrow of moved value
}
// ============================================
// PART 3: MOVE ON FUNCTION CALL
// ============================================
fn takes(s: String) {
println!("{}", s);
} // s is dropped here
fn main() {
let s = String::from("hello");
takes(s);
// s is no longer valid
}
// ============================================
// PART 4: RETURN TRANSFERS OWNERSHIP
// ============================================
fn gives() -> String {
String::from("hello")
}
fn main() {
let s = gives();
println!("{}", s);
}
// ============================================
// PART 5: COPY TYPES
// ============================================
fn main() {
let x = 5;
let y = x;
println!("{} {}", x, y); // both valid
let point = (1, 2);
let copied = point;
println!("{:?} {:?}", point, copied); // both valid
}
// ============================================
// PART 6: CLONE FOR DEEP COPY
// ============================================
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{} {}", s1, s2); // both valid
}
// ============================================
// PART 7: DERIVE COPY
// ============================================
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1;
println!("{:?} {:?}", p1, p2); // both valid
}
// ============================================
// PART 8: DROP TRAIT
// ============================================
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropping {}", self.name);
}
}
fn main() {
let r = Resource { name: String::from("file") };
println!("Before drop");
// r is dropped at the end of main
}
// ============================================
// PART 9: EXPLICIT DROP
// ============================================
fn main() {
let r = Resource { name: String::from("file") };
drop(r); // drops here, not at end of scope
// r is no longer valid
println!("After drop");
}
// ============================================
// PART 10: BORROWING INSTEAD OF MOVING
// ============================================
fn length(s: &str) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let n = length(&s); // borrows, does not move
println!("{} has {} characters", s, n); // s is still valid
}
These ten parts cover ownership basics, move on assignment, move on function call, return transferring ownership, Copy types, Clone for deep copy, deriving Copy, the Drop trait, explicit drop, and borrowing instead of moving.
Quick Reference
The Three Rules
| Rule | Meaning |
|---|---|
| One owner | Each value has exactly one owner |
| Move on transfer | Ownership moves on assignment, call, return |
| Drop on scope end | The value is dropped when the owner goes out of scope |
Move Contexts
| Context | Example |
|---|---|
| Assignment | let s2 = s1; |
| Function call | takes(s); |
| Return | return s; |
| Struct field | Point { s } |
Copy vs Move
| Trait | Behavior | Types |
|---|---|---|
Copy | Implicit bitwise copy | Scalars, &T, tuples of Copy |
| Move | Ownership transfers | String, Vec, Box |
Clone | Explicit deep copy | Any type with clone |
Drop
| Aspect | Detail |
|---|---|
| When | End of owner’s scope |
| How | Compiler-inserted destructor |
| Custom | Implement Drop trait |
| Early | std::mem::drop(value) |
Memory Safety Guarantees
| Guarantee | Mechanism |
|---|---|
| No use-after-free | Value cannot be used after owner drops |
| No double-free | One owner, one drop |
| No leaks | Drop inserted at scope end |
| Thread safety | Ownership extends to threads |
Best Practices
✅ Do This:
// Let ownership move by default
let s2 = s1;
// Use Copy types freely
let y = x;
// Clone when the original is needed
let s2 = s1.clone();
// Borrow when the function does not need ownership
fn length(s: &str) -> usize { s.len() }
// Implement Drop for resources
impl Drop for Resource { fn drop(&mut self) { ... } }
❌ Don’t Do This:
// Use a moved value
let s2 = s1;
println!("{}", s1); // ❌ s1 is moved
// Clone when a move suffices
let s2 = s1.clone(); // ❌ if s1 is not used after
// Implement Copy with Drop
#[derive(Copy)]
struct R; // ❌ if R implements Drop
// Forget to handle resources
// Rely on the compiler to drop; do not ignore the Drop trait
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Use after move | The value was transferred | Use a reference or clone |
| Clone when move works | Not tracking usage | Move when the original is unused |
| Copy type expected to move | Copy types are copied | No fix needed; behavior is correct |
Cannot implement Copy | Type implements Drop | Remove Drop or do not derive Copy |
| Drop order confusion | Drop happens at scope end | Drop order is reverse of declaration |
| Unexpected clone | Signature takes String | Change to &str |
Real-World Examples
1. Move into Function
fn process(s: String) { println!("{}", s); }
2. Borrow to Avoid Move
fn length(s: &str) -> usize { s.len() }
3. Return Ownership
fn create() -> String { String::from("new") }
4. Clone for Independence
let copy = original.clone();
5. Derive Copy
#[derive(Copy, Clone)]
struct Point { x: i32, y: i32 }
6. Custom Drop
impl Drop for Connection {
fn drop(&mut self) { self.close(); }
}
7. Explicit Drop
drop(file); // close before scope ends
8. Move in Struct
struct Wrapper { value: String }
let w = Wrapper { value: s }; // s moved into w
9. Move in Collection
let v = vec![s1, s2]; // both strings moved into the vector
10. Ownership in Iterator
let upper: Vec<String> = names.into_iter().map(|s| s.to_uppercase()).collect();
// names is consumed
Visual
The Ownership Model
┌──────────────────────────────────────────────────────────────┐
│ let s = String::from("hello"); │
│ │
│ STACK HEAP │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ s: String │ │ "hello" │ │
│ │ ptr ───────────────┼───────▶│ │ │
│ │ len: 5 │ │ │ │
│ │ cap: 5 │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ One owner: s is responsible for freeing the heap │
│ When s goes out of scope, the heap is freed │
└──────────────────────────────────────────────────────────────┘
Move on Assignment
┌──────────────────────────────────────────────────────────────┐
│ let s1 = String::from("hello"); │
│ let s2 = s1; │
│ │
│ BEFORE: │
│ s1 ──────▶ heap "hello" │
│ │
│ AFTER: │
│ s1 ╳ (invalid) │
│ s2 ──────▶ heap "hello" │
│ │
│ The stack part is copied; the heap pointer moves. │
│ s1 is invalidated; only s2 can free the heap. │
└──────────────────────────────────────────────────────────────┘
Copy vs Clone
┌──────────────────────────────────────────────────────────────┐
│ COPY (i32, bool, &T): │
│ let x = 5; │
│ let y = x; │
│ ┌────┬────┐ │
│ │ x │ y │ both valid, independent copies │
│ │ 5 │ 5 │ │
│ └────┴────┘ │
│ │
│ MOVE (String, Vec): │
│ let s1 = String::from("hi"); │
│ let s2 = s1; │
│ ┌────┬────┐ │
│ │ s1 │ s2 │ s1 invalid, s2 owns │
│ │ ╳ │ ──▶│ │
│ └────┴────┘ │
│ │
│ CLONE (explicit): │
│ let s2 = s1.clone(); │
│ ┌────┬────┐ │
│ │ s1 │ s2 │ both valid, separate heap allocations │
│ │ ──▶│ ──▶│ │
│ └────┴────┘ │
└──────────────────────────────────────────────────────────────┘
Scope and Drop
┌──────────────────────────────────────────────────────────────┐
│ fn main() { │
│ let a = String::from("first"); ┐ │
│ { │ │
│ let b = String::from("second");│ b owns "second" │
│ } ← b drops here ┘ │
│ // ... │
│ } ← a drops here │
│ │
│ Drop order is reverse of declaration (last in, first out) │
└──────────────────────────────────────────────────────────────┘
Ownership in Functions
┌──────────────────────────────────────────────────────────────┐
│ TAKES OWNERSHIP: │
│ fn process(s: String) { } │
│ └── Caller loses access after the call │
│ │
│ BORROWS: │
│ fn process(s: &str) { } │
│ └── Caller retains ownership │
│ │
│ RETURNS OWNERSHIP: │
│ fn create() -> String { } │
│ └── Caller becomes the owner │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Ownership rule 1 | Each value has one owner |
| Ownership rule 2 | Ownership moves on transfer |
| Ownership rule 3 | Value drops when owner goes out of scope |
| Move contexts | Assignment, function call, return, struct field |
Copy types | Scalars, &T, tuples and arrays of Copy |
Non-Copy types | String, Vec, Box |
Clone | Explicit deep copy |
Drop | Called when the owner goes out of scope |
std::mem::drop | Explicit early drop |
| Use-after-free | Prevented by ownership |
| Double-free | Prevented by single owner |
| Memory leaks | Prevented by automatic drop |
Key takeaways:
- Every value has exactly one owner. The owner is responsible for the value’s lifetime and for freeing any heap allocation when the value is dropped.
- Ownership moves on transfer. Assignment, function call, return, and struct initialization all move ownership. The original binding becomes invalid, and the compiler rejects any use of it.
- Values drop when the owner goes out of scope. The drop is automatic and deterministic. For heap values, it frees the allocation. There is no garbage collector.
Copytypes are copied, not moved. Scalars, references, and aggregates ofCopytypes are duplicated on assignment, so both bindings remain valid.Cloneis explicit duplication. It allocates new memory and copies the contents. It is used when the original must remain valid, and it is more expensive than a move.- A type cannot be
Copyif it implementsDrop. Dropping a copy would free the same resource twice, so the compiler forbids the combination. - Functions take ownership, borrow, or return. A
Stringparameter takes ownership; a&strparameter borrows. The choice determines whether the caller can use the value after the call. - Ownership guarantees memory safety at compile time. No use-after-free, no double-free, no leaks. The checks are static, and the runtime cost is zero.
Remember: Ownership is the rule that makes Rust’s memory safety possible without a garbage collector. Each value has one owner; the owner frees the value when it goes out of scope; and ownership moves rather than duplicates. These three rules eliminate the memory bugs that plague C and C++: use-after-free, double-free, and leaks. The rules are checked at compile time, so there is no runtime overhead. Moves are cheap because only the stack part is copied and the heap part stays where it is. Clones are explicit and allocate new memory. The Copy trait marks types that are duplicated on assignment. The Drop trait lets types define what happens when they are freed. Understanding ownership is understanding the flow of memory in a Rust program: who owns each value, when it moves, and when it is dropped. Every other Rust feature — borrowing, lifetimes, thread safety — builds on this foundation.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!