Hedronite Lesson · Polyglot-Dev / Rust · Tue 2026-09-01

Understanding Ownership one owner drop move Copy — the copy that is not a copy

The equals sign looks like a copy. For a String it is a move. Keep both names only when the type implements Copy, or when you clone.

Lesson Class: Duha (Rust language track)
Focus: three rules · stack/heap · String/drop · move · clone · Copy · function transfer
Code Blocks: clean blocks, explanation in prose
Done-criteria: predict a move versus a copy · say why a value has one owner
Grounding: TRPL stable Ch.4 HTML ch04-00, ch04-01 · Blandy not cited
The rules
Each value has one owner. Only one at a time. When the owner leaves scope, drop runs.
The move
String assignment copies pointer, length, and capacity, then invalidates the first name. That is the copy that is not a copy.
The copy
Copy types stay valid after assignment. clone copies the heap on purpose. Functions move or copy the same way let does.
The equals sign looks like a copy. For a String it is a move. Keep both names only when the type implements Copy, or when you clone.

<!-- hal:authoritative:yaml -->

The equals sign looks like a copy. For a String it is a move. Keep both names only when the type implements Copy, or when you clone.

§I — Frame

Duha session 04. Fourth fire of the TRPL spine. The chapter is Understanding Ownership. This fire is the first half: What Is Ownership. Session 05 takes references, borrowing, and slices. Stop at the door. Do not walk through it today.

Session 03 left you with mutability, types, functions, and control flow. Those binds still hold. Do not reopen variables or loops. The book’s error listing compiles a crate named ownership. Use that name if you type along. The prior crates are background, not the work.

Coin the name now: the copy that is not a copy.

let y = x; looks like a copy. For an integer it is one. For a String the compiler copies the pointer, the length, and the capacity that sit on the stack, then it invalidates the first name. The heap bytes stay where they were. The book names that a move. If you treat the first name as still live, you get error[E0382]: borrow of moved value. That error is the chapter’s hinge. Hold it while you walk the rest.

Done-criteria from the syllabus: you can predict a move versus a copy, and you can say why a value has one owner.

§II — Stack and heap, one beat

The book pauses before the rules. Stack and heap are both memory your code may use at runtime. They are not the same shape.

The stack is last in, first out. Push on top. Pop from the top. Every value on the stack has a known, fixed size. Integers, booleans, char, and a tuple of those go here.

The heap holds data whose size is unknown at compile time, or whose size may change. You ask the allocator for space. It finds a hole, marks it in use, and returns a pointer: the address of that hole. The pointer has a known size, so the pointer itself can live on the stack. The bytes it names live on the heap. Follow the pointer when you need the bytes.

Pushing on the stack is faster than allocating on the heap. The next stack slot is always the top. Heap allocation searches, then books the hole. Access on the heap is generally slower because you follow a pointer, and because processors prefer data that sits near other data.

A function call pushes its arguments and locals onto the stack. Return pops them.

Ownership’s main job is heap data: who is using it, how many copies of it exist, and when it may be returned to the allocator. Once the rules are in your hands you will not think about stack versus heap on every line. The distinction is why the rules exist.

§III — Three rules, then a scope

Keep three rules in mind. The rest of the chapter is examples of these three.

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

A scope is the range in which an item is valid. Listing 4-1 uses a string literal:

{
    let s = "hello";   // s is valid from this point forward
    // do stuff with s
}                      // this scope is now over, and s is no longer valid

Two points in time: s is valid when it comes into scope. It stays valid until it goes out of scope. That pairing is familiar. The String type is where the pairing starts to cost something.

The book drops fn main() from most listings after this. Put the examples inside main yourself if you compile them.

§IV — The String type, then drop

Chapter 3’s types had a known size. They sit on the stack. They pop when the scope ends. They copy cheaply if another name needs the same value. Heap data is the other case. String is the book’s example. Non-ownership details of String wait for Chapter 8.

A string literal is hardcoded into the executable. Fast. Immutable. Useless when the text arrives at runtime, or when the text must grow. String manages a heap buffer whose size is unknown at compile time:

let s = String::from("hello");

:: namespaces from under String. This kind of string can be mutated:

let mut s = String::from("hello");
s.push_str(", world!");
println!("{s}");

push_str appends a literal. The print is hello, world!.

Why can String grow when a literal cannot? Memory. A literal’s bytes are baked into the binary. You cannot bake a blob for every piece of text whose size you will only know later. String::from asks the allocator for a heap buffer. That is the first half. The second half is returning that buffer when you are done.

Languages with a garbage collector track unused memory for you. Languages without one ask you to pair every allocate with exactly one free. Forget, and you leak. Free too early, and you hold an invalid name. Free twice, and you corrupt the heap.

Rust’s path: the memory returns when the variable that owns it goes out of scope.

{
    let s = String::from("hello"); // s is valid from this point forward
    // do stuff with s
}                                  // scope over; s no longer valid

At the closing curly brace Rust calls drop. String’s drop returns the buffer to the allocator. You do not write the call. The compiler inserts it.

C++ names a close cousin Resource Acquisition Is Initialization (RAII). If you have used RAII, drop at the brace is the same shape.

The pattern looks simple until two names want the same heap. That is the next section.

§V — The copy that is not a copy

Listing 4-2 assigns an integer:

let x = 5;
let y = x;

Bind 5 to x. Copy the value in x and bind it to y. Two names, both 5, both on the stack. Integers have a known, fixed size. The copy is cheap. Both names stay valid.

Now the String version:

let s1 = String::from("hello");
let s2 = s1;

It looks the same. A reader who just finished Listing 4-2 will assume s2 is a copy of s1. That is the copy that is not a copy.

A String on the stack is three fields: a pointer to the heap bytes, a length, and a capacity. Length is how many bytes the contents currently use. Capacity is how many bytes the allocator has granted. Figure 4-1 in the book draws s1 that way for "hello": pointer, 5, 5, and five bytes on the heap.

let s2 = s1; copies those three stack fields. It does not copy the heap. Figure 4-2 is two stack headers pointing at the same buffer. Figure 4-3 is what a heap copy would look like: two buffers. Rust does not do Figure 4-3 automatically. A large heap copy on every assignment would be expensive. The book’s design choice: Rust will never automatically create a deep copy. Any automatic copy can be assumed cheap.

Now the trap. When a variable goes out of scope, Rust calls drop and frees the heap. Figure 4-2 has two pointers at one buffer. If s1 and s2 both stay valid, both call drop. That is a double free. Memory corruption. A security bug in other languages.

After let s2 = s1;, Rust treats s1 as no longer valid. Nothing to free when s1’s scope ends. Try to use it:

let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:16
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`,
  |            which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{s1}, world!");
  |                ^^ value borrowed here after move
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

The help line offers clone. Read it as a cost, not a default.

If you have heard shallow copy and deep copy, copying the pointer, length, and capacity without copying the heap sounds like a shallow copy. Because Rust also invalidates the first variable, the book names it a move. s1 was moved into s2. Figure 4-4 greys out s1. Only s2 will drop.

Reassignment is the inverse of a move. Bind s to "hello", then assign "ahoy":

let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");

Nothing refers to the original heap. Rust calls drop on "hello" immediately. The print is ahoy, world!. Scope end is not the only time drop runs.

§VI — Clone when you mean the heap, Copy when the stack is the whole value

If you want the heap copied, say so:

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");

That is Figure 4-3, done on purpose. clone is visible. Visible means the cost is in the source, not hidden behind =.

Integers still work without clone:

let x = 5;
let y = x;
println!("x = {x}, y = {y}");

No move. x stays valid. The value is known-size on the stack. A “deep” copy and a “shallow” copy are the same bytes. clone would do no extra work, so the usual assignment already copies.

The marker for that behavior is the Copy trait. If a type implements Copy, assignment copies and the old name stays valid. Rust will not let a type be Copy if it, or any part of it, implements Drop. A type that needs special work at end of scope cannot also be a trivial stack copy. Appendix C covers how to derive Copy. Chapter 10 covers traits. Today the rule is enough.

Types that implement Copy, as a general rule: simple scalars, and nothing that allocates or holds a resource.

Predict from that list. let y = x; on an i32 is a copy. let s2 = s1; on a String is a move. (i32, i32) copies. (i32, String) moves.

§VII — Functions take ownership the same way assignment does

Passing a value to a function moves or copies, just as let does. Listing 4-3:

fn main() {
    let s = String::from("hello");
    takes_ownership(s);             // s moves in; no longer valid here
    let x = 5;
    makes_copy(x);                  // i32 is Copy; x stays valid
}

fn takes_ownership(some_string: String) {
    println!("{some_string}");
} // some_string dropped here

fn makes_copy(some_integer: i32) {
    println!("{some_integer}");
}

Use s after takes_ownership and you get the same E0382. Use x after makes_copy and it compiles. Try both. The static check is the lesson.

Return values transfer ownership the other way. Listing 4-4:

fn main() {
    let s1 = gives_ownership();
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2); // s2 moves in; return moves into s3
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string
}

At the end of main, s3 drops. s2 was moved, so nothing happens for s2. s1 drops. The last expression is the return, as Chapter 3 already taught. Ownership rides that expression out of the function.

The pattern is the same every time: assigning a value to another variable moves it, unless the type is Copy. When a heap value’s owner goes out of scope, drop runs, unless ownership has already moved.

Taking ownership and handing it back on every call is tedious. Listing 4-5 returns a tuple so you can keep the String and also keep a length:

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{s2}' is {len}.");
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

That is too much ceremony for “use this value, then let me use it again.” The book’s next sentence names the feature that removes the ceremony: references. Session 05. Leave the tuple in this folder as the itch, not as the cure.

§VIII — Two proofs

The syllabus names two picks. Run them from the book.

Proof 1. Move versus copy.

let y = x; on a Copy type leaves both names valid. Integers, bool, floats, char, and tuples of those. let s2 = s1; on a String copies the stack header and invalidates s1. That is the copy that is not a copy. clone copies the heap on purpose. If you cannot say, before cargo run, whether the next let is a move or a copy, walk §V and §VI again.

Proof 2. One owner.

Each value has an owner. Only one at a time. When that owner’s scope ends, drop runs, unless the value has moved. Two live owners of one heap buffer would double-free. The compiler refuses the second live name. Reassignment drops the old value immediately because nothing owns it anymore. A function argument is an owner. A return value is an owner. If you explain ownership only as “Rust frees memory for you,” you missed the one-owner rule that makes the free safe.

§IX — Closing

Ch.4’s first half is the rule every later chapter assumes. One owner. Drop at end of scope, or at reassignment, or when a function’s parameter ends. Assignment of a String is a move. Assignment of an i32 is a copy. clone is the heap copy you write by hand.

Name it when let s2 = s1; looks like Listing 4-2 and behaves like Figure 4-4: the copy that is not a copy. Ask whether the type implements Copy. If it does not, the first name is dead.

Session 05 is TRPL Ch.4 still: references, borrowing, slices. How to use a value without taking it. Do not start it in this folder. Do not write it today.

Examine well. The three rules are the first proof’s ground. The String header on the stack is the picture. The E0382 on s1 after the move is the door. The Copy list is how you predict. The function listings are the same rule at a call boundary.

Related

🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-09-01 · Duha · session 04 · TRPL Ch.4 · the copy that is not a copy