Ownership · about 40 min

Ownership Through Functions

Why this matters

Programs are built from functions, and data flows between them all the time: you pass a line of input to a parser, get back a result, hand it to something that stores it. In Rust every such hand-over is an ownership decision. This lesson shows the rules for parameters and return values, which are the same move rules you already know, and then shows their limitation, which is the motivation for the whole next module on borrowing.

Passing a value moves it

Passing an argument works exactly like assignment: Copy types are copied, everything else is moved into the parameter.

fn takes_ownership(s: String) {
    println!("I own {s:?} now");
}   // s goes out of scope here: the String is dropped and its buffer freed

fn makes_copy(n: i32) {
    println!("I have a copy of {n}");
}

fn main() {
    let text = String::from("hello");
    takes_ownership(text);
    let number = 5;
    makes_copy(number);
    println!("number is still usable: {number}");
}
I own "hello" now
I have a copy of 5
number is still usable: 5

In memory, at the moment takes_ownership runs:

 takes_ownership frame          heap
 +--------------------+
 | s:  ptr  ----------------->  [h|e|l|l|o]
 |     len 5          |
 |     cap 5          |
 +--------------------+
 | main frame         |
 | text: (moved)      |
 | number: 5          |
 +--------------------+

The 24-byte handle was copied into the new frame and text is invalid. When takes_ownership returns, s is dropped and the buffer is freed. Back in main, using text would be E0382:

fn takes_ownership(s: String) {
    println!("{s}");
}

fn main() {
    let text = String::from("hello");
    takes_ownership(text);
    println!("{text}");
}
error[E0382]: borrow of moved value: `text`
 --> main.rs:8:15
  |
6 |     let text = String::from("hello");
  |         ---- move occurs because `text` has type `String`, which does not implement the `Copy` trait
7 |     takes_ownership(text);
  |                     ---- value moved here
8 |     println!("{text}");
  |               ^^^^^^ value borrowed here after move
  |
note: consider changing this parameter type in function `takes_ownership` to borrow instead if owning the value isn't necessary
 --> main.rs:1:23
  |
1 | fn takes_ownership(s: String) {
  |    ---------------    ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     takes_ownership(text.clone());
  |                         ++++++++

Look at the note: rustc points at the function parameter that took ownership, and suggests changing it to borrow instead. That is a strong hint about where this is going.

Returning a value moves it out

Return values transfer ownership to the caller:

fn make_greeting(name: &str) -> String {
    let mut g = String::from("Hello, ");
    g.push_str(name);
    g
}

fn main() {
    let greeting = make_greeting("Ada");
    println!("{greeting}");
}
Hello, Ada

g's buffer is allocated inside make_greeting and lives on in main as greeting. Because g was moved out, the function does not drop it. No copy of the text is made: only the 24-byte handle travels, typically in registers. This is how Rust functions create and return heap data cheaply.

Taking ownership and giving it back

What if a function needs to look at a String, and the caller still needs it afterwards? With only moves, the function has to return it:

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

fn main() {
    let s1 = String::from("ownership");
    let (s1, len) = length_of(s1);
    println!("{s1:?} has {len} bytes");
}
"ownership" has 9 bytes

It works, but it is clumsy: every function that merely reads its argument must hand it back, and every caller must catch it. Imagine a function that needs three Strings for reading.

There is one situation where "take and give back" is exactly right: functions that transform a value. Taking ownership lets them modify it in place without copying, and returning it hands over the result:

fn shout(mut s: String) -> String {
    s.make_ascii_uppercase();
    s.push('!');
    s
}

fn main() {
    let msg = String::from("hello");
    let msg = shout(msg);
    println!("{msg}");
}
HELLO!

mut s: String makes the parameter's binding mutable. The buffer is reused (no new allocation unless it must grow), and the caller cannot accidentally keep using the old, pre-transformation value, because it was moved. This "consume and return" style is common in Rust APIs, especially builders.

Choosing a parameter type

So far you have a choice between two parameter styles:

parametermeaning for the calleruse when
s: String"I take it; you lose it"the function stores the value, transforms and returns it, or is its final user
s: &str"I only look at it"the function only reads the text

You have already used &str parameters (make_greeting(name: &str)) without thinking about it: passing a string literal or &some_string lets the function read the text without taking ownership. That is a borrow, and it is the subject of the next module. The table above will be completed there with &mut, "I want to modify it, but you keep it".

A function that takes ownership but only reads is a design smell: it forces callers to clone or to give up their value for no reason. When a function signature says String, it should mean "I need to own this".

Ownership and data flowing through a program

Putting it together, here is a pipeline where each step owns the data while it works on it:

fn normalize(mut s: String) -> String {
    s = s.trim().to_lowercase();
    s
}

fn tag(s: String, n: usize) -> String {
    format!("{n}: {s}")
}

fn main() {
    let inputs = ["  Apple ", "BANANA", " Cherry"];
    let mut out = Vec::new();
    for (i, raw) in inputs.iter().enumerate() {
        let s = String::from(*raw);
        let s = normalize(s);
        let s = tag(s, i + 1);
        out.push(s);
    }
    println!("{:?}", out);
}
["1: apple", "2: banana", "3: cherry"]

Each String is created, moved through normalize and tag, and finally moved into the vector, which owns it until the vector itself is dropped at the end of main. At every point exactly one variable owns each buffer. (trim().to_lowercase() creates a new String; assigning it to s drops the old one. *raw follows the reference that .iter() produces, as in module 3.)

Common mistakes

Using an argument after the call (E0382). Decide: should the function borrow instead? Should the caller use the value before the call? Is a clone really needed?

Forgetting mut on a parameter you modify:

fn shout(s: String) -> String {
    s.push('!');
    s
}

fn main() {
    println!("{}", shout(String::from("hi")));
}
error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
 --> main.rs:2:5
  |
2 |     s.push('!');
  |     ^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
1 | fn shout(mut s: String) -> String {
  |          +++

The parameter binding is immutable unless declared mut s: String. Owning a value does not by itself make the binding mutable.

Returning a String for read-only work. "Take and give back" for functions that only read is a sign you want &str; next module.

Recap

Going further: take the module 3 calculator or statistics program and identify every place ownership moves. Then read the Rust Book section 4.1 "Ownership and Functions" and 4.2 "References and Borrowing", which is where we go next.

Exercises

Exercise 1 · write code

Use it after the call

describe takes ownership of its String and returns nothing, so main cannot use title afterwards (E0382). Change describe so that it gives the String back to the caller (return it alongside nothing else), and change main to receive it, so the program prints:

"Dune" has 4 bytes
title is still Dune

Do not use clone.

File: main.rs

Starting point

fn describe(s: String) {
    println!("{:?} has {} bytes", s, s.len());
}

fn main() {
    let title = String::from("Dune");
    describe(title);
    println!("title is still {title}");
}

Example

expected output

"Dune" has 4 bytes
title is still Dune
Hint 1

Give describe a return type -> String and end its body with s.

Hint 2

In main: let title = describe(title); takes ownership back (shadowing is fine).

Reference solution
fn describe(s: String) -> String {
    println!("{:?} has {} bytes", s, s.len());
    s
}

fn main() {
    let title = String::from("Dune");
    let title = describe(title);
    println!("title is still {title}");
}

Exercise 2 · write code

Take, change, give back

Write fn add_suffix(mut s: String, suffix: &str) -> String that appends suffix to s (with push_str) and returns it. No new String may be allocated inside the function: modify the one you were given.

The given main reads a word from stdin and prints the result of adding .bak and then .old:

notes.bak.old

for input notes.

File: main.rs

Starting point

use std::io;

fn add_suffix(s: String, suffix: &str) -> String {
    s
}

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let name = line.trim().to_string();
    let name = add_suffix(name, ".bak");
    let name = add_suffix(name, ".old");
    println!("{name}");
}

Example

input

notes

expected output

notes.bak.old
Hint 1

The parameter must be mut s: String to call s.push_str(...) on it.

Hint 2

s.push_str(suffix); s appends and returns the same String.

Reference solution
use std::io;

fn add_suffix(mut s: String, suffix: &str) -> String {
    s.push_str(suffix);
    s
}

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let name = line.trim().to_string();
    let name = add_suffix(name, ".bak");
    let name = add_suffix(name, ".old");
    println!("{name}");
}

Exercise 3 · quiz

Where does the buffer go?

fn make() -> String {
    let s = String::from("made in make");
    s
}

fn main() {
    let t = make();
    println!("{t}");
}

Which statement is true?

Answer
  • Returning s moves ownership to the caller; the buffer is not freed until t goes out of scope at the end of main

Returning a value is a move, just like assignment. Ownership of the heap buffer passes from s to t, and because s was moved out, it is not dropped at the end of make. The buffer is created in one function and freed in another, with no copying and no garbage collector, and the compiler knows exactly where the free happens.

Exercise 4 · write code

Keep one, drop the other

Write fn longer(a: String, b: String) -> String that returns whichever String is longer (the first one if they are equal). The other one is simply dropped when the function returns.

The given main reads two lines from stdin and prints the longer one:

longer: elephant

for the input lines cat and elephant.

File: main.rs

Starting point

use std::io;

fn read() -> String {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    line.trim().to_string()
}

fn main() {
    let a = read();
    let b = read();
    println!("longer: {}", longer(a, b));
}

Example

input

cat
elephant

expected output

longer: elephant

Example

input

abc
xyz

expected output

longer: abc
Hint 1

if b.len() > a.len() { b } else { a }: one is moved out as the return value, the other dropped.

Reference solution
use std::io;

fn read() -> String {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    line.trim().to_string()
}

fn longer(a: String, b: String) -> String {
    if b.len() > a.len() { b } else { a }
}

fn main() {
    let a = read();
    let b = read();
    println!("longer: {}", longer(a, b));
}