| |

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.

TypeCopyBehavior on assignment
i32, u64, f64YesCopy
bool, charYesCopy
(i32, i32)YesCopy
[u8; 4]YesCopy
&TYesCopy (the reference)
StringNoMove
Vec<T>NoMove
Box<T>NoMove
&mut TNoMove

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

RuleMeaning
One ownerEach value has exactly one owner
Move on transferOwnership moves on assignment, call, return
Drop on scope endThe value is dropped when the owner goes out of scope

Move Contexts

ContextExample
Assignmentlet s2 = s1;
Function calltakes(s);
Returnreturn s;
Struct fieldPoint { s }

Copy vs Move

TraitBehaviorTypes
CopyImplicit bitwise copyScalars, &T, tuples of Copy
MoveOwnership transfersString, Vec, Box
CloneExplicit deep copyAny type with clone

Drop

AspectDetail
WhenEnd of owner’s scope
HowCompiler-inserted destructor
CustomImplement Drop trait
Earlystd::mem::drop(value)

Memory Safety Guarantees

GuaranteeMechanism
No use-after-freeValue cannot be used after owner drops
No double-freeOne owner, one drop
No leaksDrop inserted at scope end
Thread safetyOwnership 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

PitfallWhy It HappensFix
Use after moveThe value was transferredUse a reference or clone
Clone when move worksNot tracking usageMove when the original is unused
Copy type expected to moveCopy types are copiedNo fix needed; behavior is correct
Cannot implement CopyType implements DropRemove Drop or do not derive Copy
Drop order confusionDrop happens at scope endDrop order is reverse of declaration
Unexpected cloneSignature takes StringChange 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

ItemValue
Ownership rule 1Each value has one owner
Ownership rule 2Ownership moves on transfer
Ownership rule 3Value drops when owner goes out of scope
Move contextsAssignment, function call, return, struct field
Copy typesScalars, &T, tuples and arrays of Copy
Non-Copy typesString, Vec, Box
CloneExplicit deep copy
DropCalled when the owner goes out of scope
std::mem::dropExplicit early drop
Use-after-freePrevented by ownership
Double-freePrevented by single owner
Memory leaksPrevented 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.
  • Copy types are copied, not moved. Scalars, references, and aggregates of Copy types are duplicated on assignment, so both bindings remain valid.
  • Clone is 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 Copy if it implements Drop. Dropping a copy would free the same resource twice, so the compiler forbids the combination.
  • Functions take ownership, borrow, or return. A String parameter takes ownership; a &str parameter 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!