Before you begin
Why this matters
When the compiler rejects a function that returns a reference, the fix depends on one question: which value does the result point into? If you can answer it from the code, you can write the signature, predict which calls the compiler accepts, and tell when no annotation can help and the function must return an owned value.
What this helps you reason about
- Write a signature that says which input a returned reference points into, and predict which calls compile.
- Say when you can leave the annotations out, and what a struct that holds a reference prevents.
- Recognize when no annotation can help, including adding 'static, and return or store an owned value instead.
In Rust, the owner frees each value at a point you can see, and the compiler checks that no reference is used after that. To check a call, the compiler must know where a returned reference points. A lifetime annotation such as 'a says which value that is; it sets no time. This lesson reads annotations as those relationships.
Already know this? The short version
- A lifetime annotation such as 'a names a relationship: the result, or a struct’s field, points into the inputs marked with the same name. It changes no lifetime. The compiler checks it in the body and at every call, and it is gone from the compiled program.
- Mark only the inputs the result can really point into. An unmarked input may be dropped while the result is still used; marking it too gives E0597 at such a call.
- Elision: with one reference input, or a method taking &self, the result is assumed to point into that input or into self. A method that returns a different parameter needs the names written out.
- A struct holding a reference needs a lifetime parameter, struct Sighting<'a> { bird: &'a str }, and its source cannot change or be dropped while the struct is used. When a result is new data, or must outlive its source, return or store an owned value; 'static never keeps a local alive.
- The section “What goes wrong with lifetimes in real programs?” covers adding 'static to silence an error, keeping slices of a buffer you reuse, structs that borrow from text the function itself read, and positions kept instead of references.
Words in this lesson (5)
- lifetime
- The part of the program during which a reference may be used. It must end before the value the reference points to is dropped.
- lifetime annotation
- A name such as 'a, written in a function’s signature, that says which inputs a returned reference may point into. It describes lifetimes; it never makes a value live longer.
- string slice
- A reference to some text, written &str: all or part of a String, or a string literal. &entry[6..] is a string slice of entry’s text from byte 6 to the end.
- lifetime elision
- The rules by which the compiler fills in lifetime annotations that a signature leaves out. With exactly one reference input, the result points into it; in a method that takes &self, the result points into self.
- 'static
- The lifetime of values that last for the whole run of the program, such as a string literal. A reference to a local variable is never 'static.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Relationships
What does a lifetime annotation describe?
Rust borrowing and references showed that a function with two reference inputs needs a lifetime annotation to return a reference, as in fn longer<'a>(a: &'a str, b: &'a str) -> &'a str. This lesson looks at what that annotation says, and what it does not say.
A function cannot lend out a value it made itself, because that value is dropped when the function returns. So a reference that a function returns must point into one of its inputs. The annotation says which one. In bird below, <'a> after the name declares a lifetime name. Writing 'a on entry and on the result says: the result points into entry. place has no 'a, so the result does not point into place.
&entry[6..] is a string slice: a &str that points into entry’s text, from byte 6 to the end. &entry[..5] is the first five bytes. Nothing is copied.
The compiler uses the signature in two places. In the body, it checks that the result really points into entry; returning place would be rejected. At each call, it checks that the result is used only while entry’s owner is alive. It does not compare the result with place, so the caller may drop place first.
'a is not a length of time, and it makes no value live longer or shorter. The stepper shows the same addresses and lengths with or without it.
fn bird<'a>(entry: &'a str, place: &str) -> &'a str {
println!("{} at {}", &entry[..5], place);
// points into entry, never into place
&entry[6..]
} A lifetime annotation says which input a returned reference points into; it changes no lifetime.
Coming from Java: A Java method may return any object it can reach, and the garbage collector keeps that object alive for the caller; nothing keeps a Rust owner alive for a reference, so the signature must say which owner the result depends on.
Coming from C: In C, only a comment says which argument a returned pointer points into; in Rust the signature says it, and the compiler checks the body and every call against it.
Precisely: what the compiler chooses for 'a
'a is a lifetime parameter, like a type parameter. At each call the compiler chooses a part of the program for it, as short as the uses of the result allow, and checks that every reference marked 'a is valid for all of it. Lifetimes are removed before machine code is made, so two functions that differ only in their annotations compile to the same code. The error messages in this lesson are quoted as rustc 1.63, the stepper’s compiler, prints them.
Experiment
Follow a returned reference back to its owner
The signature ties the result to entry only, so the compiler checks that name is used only while entry is alive, and lets place be dropped first. With 'a on place too, the signature says name may point into place, so name may not be used after place is dropped, although at run time it never points there.
Why it matters The compiler judges each call by the signature, not by what the body does at run time. Put the result’s lifetime only on the inputs the result can really point into.
Predict both output lines, and whether the program compiles although place is dropped before name is printed. Then change place: &str to place: &'a str in the first line, and predict again.
fn bird<'a>(entry: &'a str, place: &str) -> &'a str {
println!("{} at {}", &entry[..5], place);
&entry[6..]
}
fn main() {
let entry = String::from("07:15 wren");
let name;
{
let place = String::from("pond");
name = bird(&entry, &place);
}
println!("{}", name);
}
What to look for
Step into bird. entry shows "07:15 wren" (len=10) with the same data= address as entry in main’s frame; data= is the address where the text starts. place shows "pond" (len=4) with place’s data= address. Before the call, name shows "" (len=0) with an unrelated address, because name has no value yet. At the last line, place is gone from main’s frame and entry is still there. name shows "wren" (len=4), and its data= address is 6 higher than entry’s: name points into entry’s text, 6 bytes from the start. The run prints 07:15 at pond, then wren. After the edit, the program does not compile: error[E0597]: `place` does not live long enough, with “borrowed value does not live long enough” under &place, “`place` dropped here while still borrowed” at the closing brace of the inner block, and “borrow later used here” under name in the last line.
Expected output
07:15 at pond
wren02 / Elision
When can I leave the annotations out?
Most functions that return a reference need no annotation, because the compiler fills it in. This is called lifetime elision, and it follows rules you can apply yourself.
Each reference parameter gets its own lifetime. If there is exactly one reference input, the result gets that lifetime: fn year(date: &str) -> &str returns a reference into date. In a method, &self means the method borrows the value it is called on. If a method takes &self, the result gets the lifetime of self, even when there are other reference parameters.
Elision chooses a relationship from the signature alone; it does not look at the body. If a method returns a parameter instead of something inside self, the filled-in relationship is wrong. For example, fn bird_or(&self, fallback: &str) -> &str with the body fallback gives:
error: lifetime may not live long enough
with “associated function was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1`”. An associated function is any function declared inside an impl block; a method is one that takes self. '1 and '2 are names the compiler makes up for the lifetimes it filled in: '2 is the lifetime of self, and '1 is the lifetime of fallback. So the message says: the signature promised a result from self, and the body returns fallback. Write the relationship out instead, as bird_or does below.
When no rule applies, for example with two reference inputs and no &self, the compiler reports E0106, as in Rust borrowing and references.
struct Feeder {
last_bird: String,
}
impl Feeder {
// filled in: the result points into self
fn last(&self) -> &str {
&self.last_bird
}
// written out: into self or into fallback
fn bird_or<'a>(&'a self, fallback: &'a str) -> &'a str {
if self.last_bird.len() > 0 {
&self.last_bird
} else {
fallback
}
}
} With one reference input, or &self, the compiler fills in the relationship; write it out when the result can point somewhere else.
Precisely: the elision rules
The Rust Reference states three rules for functions. Each elided lifetime in the parameters becomes its own lifetime parameter. If there is exactly one input lifetime, elided or not, it is given to every elided lifetime in the result. If there are several, but one of them belongs to &self or &mut self, the lifetime of self is given to the result. Otherwise a result with an elided lifetime is an error. '_ asks for elision explicitly in a place where a lifetime is expected, as in Sighting<'_>.
03 / Structs
How does a struct hold a reference?
A struct can hold a reference too. Its type then needs a lifetime parameter, because a value of the struct is only valid while the value its field points into is alive. Leaving it out, as in struct Sighting { bird: &str }, gives:
error[E0106]: missing lifetime specifier
with “expected named lifetime parameter” under &str, and help that adds <'a> to the struct and 'a to the field.
struct Sighting<'a> says that a Sighting points into whatever bird points into. The Sighting and the text it points into are two separate values, and the borrow links them. The Sighting may be dropped first. While the Sighting is still used, the text cannot be changed, moved, or dropped.
A reference to such a struct holds two borrows. In &'b Sighting<'a>, 'b is how long the borrow of the Sighting lasts, and 'a is how long the borrow of the text lasts. bird_of below names only 'a, so its result stays usable after the Sighting is dropped. With s: &'a Sighting<'a>, the two borrows get one name, and the result may be used only while the Sighting is alive too.
So a struct with a reference suits a short-lived view of data that something else owns, such as the parts of one line while you work with that line. A struct that must be kept, returned, or stored after its source changes should own its data: bird: String.
struct Sighting<'a> {
bird: &'a str,
count: u32,
}
// the result points into the text, not into s
fn bird_of<'a>(s: &Sighting<'a>) -> &'a str {
s.bird
}
let log = String::from("07:15 wren");
let seen = Sighting { bird: &log[6..], count: 2 };
// error[E0502] here, because seen is used below:
// log.clear();
println!("{} x{}", seen.bird, seen.count); A struct that holds a reference is a borrow: its source must stay unchanged while the struct is used.
Coming from Python: A Python object can keep a reference to a list that other code keeps changing; a Rust struct that holds a reference stops every change to its source while the struct is in use.
Experiment
Keep a reference inside a struct
&log[6..] borrows log, and seen.bird stores that borrow. Sighting<'a> carries it, so the borrow lasts until the last use of seen. log.clear() needs a mutable borrow of log while seen still holds a shared one.
Why it matters Before you put a reference in a struct, ask whether its source can stay unchanged for as long as the struct is used. If it cannot, give the struct an owned field.
Predict what prints. Then change let log to let mut log, add log.clear(); on the line after let seen, and predict whether the program still compiles. clear empties a String.
struct Sighting<'a> {
bird: &'a str,
count: u32,
}
fn main() {
let log = String::from("07:15 wren");
let seen = Sighting { bird: &log[6..], count: 2 };
println!("{} x{}", seen.bird, seen.count);
}
What to look for
At the println! line, look at seen in main’s frame. Its bird field shows "wren" (len=4) with a data= address 6 higher than log’s, and log is still in the frame: the struct holds an address into log’s text, not a copy of it. Nothing in the frame shows 'a; the lifetime exists only in the source code. The run prints wren x2. After the edit, the compiler reports error[E0502]: cannot borrow `log` as mutable because it is also borrowed as immutable, with “immutable borrow occurs here” under log in &log[6..], “mutable borrow occurs here” under log.clear(), and “immutable borrow later used here” under seen.bird.
Expected output
wren x204 / Owned results
What if the result does not point into any input?
Some functions make new data: a label built from a name, or a name in lower case. A reference to that data could point only into a local of the function, which is dropped when the function returns. No annotation can describe that, because there is no input to name. The compiler reports error[E0515]: cannot return reference to local variable `text`, with “returns a reference to data owned by the current function”, as in Rust borrowing and references.
The compiler’s help can mislead here. For a function with no reference inputs, the help for E0106 suggests the 'static lifetime. 'static is the lifetime of values that last for the whole run of the program, such as a string literal written in the code. -> &'static str promises to return such a value. Writing it does not make text live longer; the same &text is rejected with E0515.
'static on a parameter does not help either. The function then accepts only references that last the whole run, so a call with &entry, where entry is a local String, gives error[E0597]: `entry` does not live long enough, with “argument requires that `entry` is borrowed for `'static`”.
The fix is to change the relationship, not the annotation. Return the String itself: ownership moves to the caller, and the text is not copied.
// not -> &str: the text is new
fn label(bird: &str) -> String {
let mut text = String::from(bird);
text.push_str(" (seen)");
text
} When the result is new data, return it owned; 'static never keeps a local alive.
Precisely: other ways to get a 'static reference
Box::leak gives up ownership of a value so that it is never freed, and returns a &'static reference to it. That is a memory leak on purpose. It suits a value made once while a program starts, not one per call. When a function sometimes returns borrowed text and sometimes new text, std::borrow::Cow<str> can hold either.
Experiment
Return new text to the caller
text is a local of label, so it is dropped when label returns unless it moves out. Returning text moves the String to the caller. 'static says the result lasts for the whole run; text does not, and the annotation cannot change that.
Why it matters When a function makes new data, return it owned. No annotation can make a reference to a local valid after the function returns.
Predict both output lines. Then change -> String to -> &'static str in the first line and the last line of label from text to &text, and predict whether 'static lets the reference outlive the function.
fn label(bird: &str) -> String {
let mut text = String::from(bird);
text.push_str(" (seen)");
println!("made {} bytes", text.len());
text
}
fn main() {
let entry = String::from("07:15 wren");
let name = label(&entry[6..]);
println!("{}", name);
}
What to look for
Step into label. bird shows "wren" (len=4) with a data= address 6 higher than entry’s: bird points into entry’s text. At the println! line inside label, text shows "wren (seen)" with its own data= address. Back in main, name shows "wren (seen)" with that same data= address: the String moved to main, and its text was not copied. The run prints made 11 bytes, then wren (seen). After the edit, the program does not compile: error[E0515]: cannot return reference to local variable `text`, with “returns a reference to data owned by the current function” under &text.
Expected output
made 11 bytes
wren (seen)05 / In production
What goes wrong with lifetimes in real programs?
Four mistakes with lifetimes are common in larger programs. If the compiler says E0502, E0515, or E0597, look here.
Adding 'static to silence an error. The help for E0106 suggests it, and the code compiles while every caller passes a string literal. The first caller with a String gets E0597. Return an owned value instead.
Keeping slices of a buffer you reuse. A loop reads each line into one String and pushes line.trim() into a Vec<&str>. The next line.clear() gives E0502, because the Vec still borrows line. Push line.trim().to_string(), as below.
Returning a struct that borrows a local. A function that reads text into a local String cannot return Sighting { bird: &text[6..], count: 1 }: that gives E0515. Give the struct owned fields, or let the caller own the text.
Keeping a position instead of a reference. An index into a Vec compiles, because the compiler cannot check a number. After insert, remove, or sort, the index names a different element, and nothing reports it. Keep an owned copy, or an id that never changes.
let mut line = String::new();
let mut birds: Vec<String> = Vec::new();
for text in ["wren ", "jay "] {
line.clear();
// stands in for reading the next line
line.push_str(text);
// E0502 if birds were a Vec<&str>
birds.push(line.trim().to_string());
} Never add 'static only to silence an error, own what must outlive its source, and do not trade a reference for a position the compiler cannot check.
Experiment
Keep a position into a list that changes
A position is a plain number. Nothing ties it to the element it came from, so the compiler has nothing to check, and insert moves every later element one place along. A reference carries a relationship, so the compiler sees that best is still used when insert changes birds.
Why it matters When a borrow error tempts you to keep a position instead, ask whether the list can change before the position is used. If it can, keep an owned copy of the element, or an id that never changes.
best was meant to be jay. Predict what prints. Then change let best = 1; to let best = &birds[1]; and the last line to println!("best: {}", best);, and predict whether it compiles.
fn main() {
let mut birds = vec![String::from("wren"), String::from("jay")];
// the best sighting, kept as a position in birds
let best = 1;
birds.insert(0, String::from("robin"));
println!("best: {}", birds[best]);
}
What to look for
Watch best and birds. best stays 1 at every stop. After insert, birds lists robin, wren, jay, so position 1 now holds wren, and the run prints best: wren. After the edit, the program does not compile: error[E0502]: cannot borrow `birds` as mutable because it is also borrowed as immutable, with “immutable borrow occurs here” under birds in &birds[1], “mutable borrow occurs here” under the insert line, and “immutable borrow later used here” under best.
Expected output
best: wren06 / Try it
Can you say what each result points into?
Find what pick points into, then decide whether the program compiles and what it prints.
struct Count<'a> {
bird: &'a str,
seen: u32,
}
fn busier<'a>(a: &Count<'a>, b: &Count<'a>) -> &'a str {
if b.seen > a.seen {
b.bird
} else {
a.bird
}
}
fn main() {
let log = String::from("wren jay");
let pick;
{
let wren = Count { bird: &log[..4], seen: 3 };
let jay = Count { bird: &log[5..], seen: 5 };
pick = busier(&wren, &jay);
}
println!("{}", pick);
}
Does the program compile, and what does it print? Would it still compile if busier’s parameters were written a: &'a Count<'a>, b: &'a Count<'a>?
Reveal the reasoning
It compiles and prints jay. busier returns b.bird, a &'a str, and 'a is the lifetime inside Count<'a>: the result points into log’s text, not into the structs wren and jay. The references &wren and &jay get their own lifetimes by elision, so wren and jay may be dropped at the end of the inner block while pick is still used. With &'a Count<'a>, the signature says the result may be used only while the borrows of wren and jay last, so the compiler rejects the program with error[E0597]: `wren` does not live long enough, and the same for jay, although pick still points into log.
Practice tickets
- RUST-301 · Every jar without a written label shows the same name → Free
- RUST-302 · Boxes labelled in capitals are missing from the room count → Sign in · Pro
- RUST-303 · Old chores show the wrong housemate after someone moves in → Sign in · Pro