Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 1 of 20

Easy Primitives

The three rules of ownership

Return a name twice — once shouted, once whispered.

pub fn shout_twice(name: String) -> Vec<String>

"Ada" becomes ["ADA", "ada"]. That is the whole algorithm. The reason this is the first problem of the track is that the starter does not compile, and the error it produces is the single most important error in Rust.

The three rules

Every rule Rust enforces about memory is downstream of three sentences:

  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 is dropped.

Most beginners can recite those and still read the borrow checker as bureaucracy. So it is worth saying plainly what problem they solve.

In C, a char* tells you nothing about who frees it. Every C codebase invents a convention — a comment, a naming scheme, a doc block — and every C codebase has bugs where the convention was misread: two frees of the same pointer, a read after a free, or a buffer nobody ever freed. In a garbage-collected language the problem is handled by never freeing anything until a runtime proves nothing can reach it, which costs you a runtime, pauses, and the ability to know when anything is released.

Rust’s three rules are a third answer: put the convention in the type system so the compiler checks it. Rule 2 is what makes it decidable. If a value could have two owners, “when does this get freed?” would need whole-program analysis. With exactly one owner, the answer is local and mechanical — and so is the check.

What “moved” means

Write this:

let first = String::from("hello");
let second = first;

Nothing is copied on the heap. String is a three-word struct on the stack — a pointer, a length, a capacity — and those three words are memcpy’d into second. Now two variables would name the same heap buffer, which rule 2 forbids, so Rust invalidates the source: first is no longer usable. The value moved.

That is why the error is called “use of moved value” and why the compiler keeps telling you a type “does not implement the Copy trait” — for a type that does, the source stays valid, because copying the bytes really does produce a second independent value. String is not such a type: its bytes contain a pointer, and two owners of the same pointer is exactly the situation rule 2 exists to prevent.

::: question Here are two versions of the same idea. Predict which compiles.

// (a)
pub fn shout_twice(name: String) -> Vec<String> {
    vec![name.to_uppercase(), name.to_lowercase()]
}

// (b)
fn upper(s: String) -> String { s.to_uppercase() }
fn lower(s: String) -> String { s.to_lowercase() }

pub fn shout_twice(name: String) -> Vec<String> {
    vec![upper(name), lower(name)]
}

(a) compiles. (b) does not — and that is not a bug in (a), it is the whole lesson.

str::to_uppercase has the signature fn to_uppercase(&self) -> String. That & means the method only looks at the string; it never takes ownership. name is untouched by the first call, so it is still there for the second.

upper takes String — no &. Calling it hands the value over for good. The second call has nothing left to hand over, and rustc says so:

error[E0382]: use of moved value: `name`
  |
  |     vec![upper(name), lower(name)]
  |                ----         ^^^^ value used here after move
  |                |
  |                value moved here

You will meet this pair — a &self method that leaves the value alone versus a by-value function that consumes it — thousands of times. Reading a signature and knowing instantly which one you are looking at is the skill this whole track builds. :::

Your job

Make the starter compile without changing upper or lower. You have exactly three moves available, and only one of them works here:

  • Give it away and never use it again. Fine when one call is enough; not enough here, because both calls need the name.
  • Have the function hand it back. fn upper(s: String) -> (String, String) would work — but you may not change those signatures.
  • Make a second value. String implements Clone, and name.clone() allocates a fresh heap buffer holding the same bytes. Two values, two owners, no rule broken.

Use the third. Cloning is explicitly allowed in this track — it is training wheels we take off deliberately in Track 3, once you have & to replace it with. What matters now is that you can say out loud what a clone costs: one allocation and one memcpy of the whole buffer, every time.

You may be tempted to write &name because rustc’s help text suggests borrowing. Resist it for now. This track exists to exhaust move / clone / hand-it-back first, so that when & arrives you already know exactly which pain it removes.

Watch out

Two of the test cases use non-ASCII text, and one of them uppercases ß into SS — two characters where there was one. Anything that reaches for as_bytes() and flips bit 5 will get these wrong. to_uppercase and to_lowercase do full Unicode case mapping; use them.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…