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 in uppercase, once in lowercase.

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

For example, "Ada" becomes ["ADA", "ada"]. The case conversion is the easy part. The starter code does not compile, and the exercise is about reading that compiler error and fixing it.

The contract

  • name can be any string, including the empty string and non-ASCII text.
  • Return a vector with exactly two elements, in this order: the result of str::to_uppercase on name, then the result of str::to_lowercase on name.
  • Those methods apply Unicode's default case mappings, which do not depend on language or locale. A conversion can change the length of a string: "ß" uppercases to "SS". Characters without case, such as digits and spaces, are unchanged.
  • An empty name gives ["", ""].

The three rules

The Rust book states ownership as three rules:

  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 (its cleanup code runs and, for a String, its heap buffer is freed).

An owner is usually a variable, a struct field or a collection element that holds the value. Borrowing with & is a separate set of rules that a later exercise covers; this exercise uses only moves and clones.

These rules address a real problem. In C, a char* does not say who is responsible for freeing the memory it points to. Programs rely on conventions in comments or names, and mistakes produce double frees, reads after a free, or memory that is never freed. Garbage-collected languages avoid freeing memory by hand: a runtime frees memory once it determines that nothing can reach it, which means you do not control exactly when that happens.

Rust takes a third approach. Ownership is part of the type system, so the compiler checks it. Because each value has exactly one owner, the compiler can decide when to drop it by looking at the owner's scope, without tracking every place the value might be shared. (Types such as Rc provide shared ownership when you need it, by keeping a reference count at run time. Each Rc handle is still an ordinary value with one owner.)

What "moved" means

Consider:

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

The text "hello" is not copied. A String value consists of three fields: a pointer to a heap buffer holding the bytes, a length and a capacity. The assignment copies those three fields into second (the optimizer may avoid even that copy). If first stayed usable, two variables would own the same heap buffer and both would try to free it, so rule 2 applies: first can no longer be used. The value has moved to second.

Some types, such as integers and bool, implement the Copy trait. For those, copying the bits produces a second independent value, so the source stays usable after an assignment or a call. String does not implement Copy, because a copy of its bits would be a second owner of the same heap buffer. That is why rustc's error message mentions Copy.

Check your understanding Here are two versions of the same idea. Predict which compiles. Show answer
// (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.

str::to_uppercase has the signature fn to_uppercase(&self) -> String. The &self means the method receives a reference to the string and does not take ownership of it. (Calling it on a String works because a String can be used where a &str is expected.) After the first call, name still owns its value, so the second call can use it.

upper takes s: String, with no &. Calling upper(name) moves the value into upper, which drops it when it returns. The second call, lower(name), uses a variable whose value has already moved, and rustc 1.98 reports (abridged, line numbers removed):

error[E0382]: use of moved value: `name`
  |
  | pub fn shout_twice(name: String) -> Vec<String> {
  |                    ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
  |     vec![upper(name), lower(name)]
  |                ----         ^^^^ value used here after move
  |                |
  |                value moved here

The full message also notes that upper could borrow its parameter instead, and suggests cloning. Whether a function takes &T or T tells you whether a call leaves your value usable or consumes it, so it is worth checking the signature whenever you see this error.

Your job

Make the starter compile and return the correct vector, keeping upper and lower unchanged and calling each of them once. (The tests check only the returned vector; keeping the helpers is what makes this an ownership exercise.) In general there are three ways to deal with a move:

  • Give the value away and stop using it. This works when one call is enough. Here both calls need the name.
  • Have the function return the value. For example, fn upper(s: String) -> (String, String) could return the uppercase text and the original string. This exercise does not allow changing the helpers' signatures.
  • Make a second value. String implements Clone, and name.clone() creates a new String with its own heap buffer containing the same bytes. There are now two values with one owner each, so no rule is broken.

Use the third option. The cost of a clone is a new heap allocation (none for an empty string) plus a copy of all the string's bytes, and you pay it each time you call clone.

rustc's note suggests changing upper to borrow its parameter (for example, fn upper(s: &str) -> String). That is often the better design, but it changes the helper's signature, which this exercise does not allow. Borrowing is covered later.

Watch out

Two of the test cases use non-ASCII text, and one of them uppercases ß into SS. Methods that work byte by byte, such as to_ascii_uppercase or flipping bits in as_bytes(), leave non-ASCII letters unchanged and fail these cases. Use to_uppercase and to_lowercase.

Your code must compile, pass the tests and pass cargo clippy -- -D warnings.