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:
| parameter | meaning for the caller | use 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
- Passing a non-Copy value to a function moves it into the parameter; the parameter drops it at the end of the function unless it is moved on.
- Returning a value moves ownership to the caller; nothing is copied but the small handle.
- "Take ownership and return it" suits functions that transform a value; it is clumsy for functions that only read.
fn f(s: String)means "I need to own this";fn f(s: &str)means "I only read it".- At every point in a program, each heap buffer has exactly one owner, and the compiler knows where it will be freed.
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.