Temporary lifetime extension¶
Level: 301 · deep dive
One line: A temporary dies at the semicolon, except after a short list of shapes in a let that quietly keep it to the end of the block instead — and a function call is not on the list.
fn main() {
let held = &String::from("kept"); // the temporary lives to the closing brace
println!("{held}"); // kept
}
Nothing owns that String. It is a temporary, and the ordinary rule would drop it at the ; — leaving held pointing at freed memory, which is E0716. Instead the & in a let extends it: the value is given the same lifetime as held itself.
The rule¶
The default is narrow and easy to state: a temporary is dropped at the end of the statement that made it. That is why let _ = Guard::acquire(); releases the guard before the next line, which is the trap Drop and RAII opens on.
Extension applies only in a let (and a static/const), and only when the initializer is an extending expression:
| Initializer | Extended | Why |
|---|---|---|
&make() / &mut make() |
yes | a borrow expression |
Holder { r: &make() } |
yes | struct literal, extending operand |
(&make(), 1) · [&make()] |
yes | tuple and array literals likewise |
&make().field |
yes | field access chains through |
&pairs()[0] |
yes | indexing chains through |
make().name() |
no | a method call |
hold(&make()) |
no | a function call |
The list is syntactic, not semantic. The compiler decides by looking at the shape of the expression you wrote, which is why the next section's two lines behave differently while doing the same thing.
The pair that differs by nothing visible¶
let inline = Holder { r: &Noisy("H") }; // compiles — struct literal extends
// let called = hold(&Noisy("W")); // E0716 — a call does not
error[E0716]: temporary value dropped while borrowed
--> not_extended.rs:11:24
|
11 | let called = hold(&Noisy("W"));
| ^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary value which is freed while still in use
12 | println!("{} {}", inline.r.0, called.r.0);
| ---------- borrow later used here
|
help: consider using a `let` binding to create a longer lived value
hold does nothing but build the same Holder. Wrapping the construction in a function is what removes the extension, and the help: line is the fix: give the temporary a name of its own, on its own line.
Extending one field keeps the whole value¶
&pair().first does not extract a field and drop the rest. The Pair temporary is extended entire, and both its fields go out of scope together at the closing brace. Section 3 of the transcript below shows both drops arriving at once.
A match holds its scrutinee through every arm¶
This is where the rule stops being trivia:
match registry.lock().unwrap().state() { // guard alive for the whole match
State::Ready => registry.lock().unwrap().start(), // ...deadlock
_ => {}
}
Temporaries in a scrutinee live until the end of the match, arms included. The same expression in a let releases at the ;:
Edition 2024 changed the if let half of this: the scrutinee's temporaries are now dropped before the else block runs, where in edition 2021 they lived to the end of the whole if/else. Same program, two editions:
--- 2024 --- --- 2021 ---
before before
[drop IF] else branch
else branch [drop IF]
after after
When there is nothing to extend¶
&Quiet("Q") needs no extension at all: with no Drop impl and no interior mutability, the value is promoted into an anonymous static, and the reference is &'static. Annotating it is the proof:
That is a different mechanism with a different rule — see const and static. It matters here because it hides the lesson: an experiment written with a plain data struct will appear to extend everything, and only gains a Drop impl's honesty once there is something to drop.
If you are coming from another language¶
- C++. This is the one place the two languages agree closely, and the C++ rule is older: binding a temporary to a
const T&(or aT&&) extends its lifetime to the reference's. The traps are the same shape too — extending through a function return does not work in either language, and C++'sconst std::string& s = f().name();dangles for the reason this page'shold(&make())does. What Rust adds is that the failure isE0716at compile time rather than a value that reads fine in testing. - Python. Nothing corresponds, because nothing is destroyed at a syntactic boundary: a temporary lives while any reference to it does, and CPython's refcount decides that at run time. The Python habit to unlearn is
with—with lock:makes the region explicit and visible, where Rust's guard is released by an invisible rule about where the expression sat. When you want the C++/Rust temporary to behave like awithblock, give it a name. - ABAP. The closest thing is the lifetime of an inline-declared result or a
NEW-created object inside an expression: garbage collection decides, and there is no statement boundary at which anything is released. So the mistake this page is about cannot happen — and neither can the guarantee.CLEANUPand explicitFREEexist because ABAP has no deterministic destructor at all; Rust has one, and this page is the list of exceptions to when it fires.
The verified output¶
Verified output of temporary_lifetimes.rs — regenerated by tools/run_examples.py, never hand-typed.
1. `&` in a let extends; anything else drops at the semicolon
before both lets
[drop B]
after both lets -> A is A, B was B
leaving the block
[drop A]
2. A struct literal is an extending expression too
Holder built inline -> H
leaving the block
[drop H]
3. Extending one field keeps the WHOLE temporary alive
made a Pair -> P1 + P2
&pair().first -> P1
leaving the block -> both fields are still here
[drop P1]
[drop P2]
4. A match holds its scrutinee's temporaries through every arm
entering the match
inside the arm -> M is still alive
[drop M]
left the match
5. ...and the same expression in a let does not
[drop S]
after the let -> S was returned, S is already gone
6. An `if let` releases its scrutinee BEFORE the else — since edition 2024
entering the if let
[drop IF]
else branch -> IF is already gone
left the if let
7. With no `Drop` and no interior mutability, there is nothing to extend
&Quiet(..) as &'static -> Q
the annotation is the proof: rvalue static promotion, not extension
See also¶
Drop, and what RAII buys — thelet _ =trap, which is this rule with no extension appliedconstandstatic— promotion, the mechanism that hides this one- Scope is about names — the three things "out of scope" is asked to mean
- Lifetime annotations —
'anames the relationship that extension is quietly satisfying
Sources¶
The Reference on temporary lifetime extension ↗ and temporary scopes ↗; the if let change is RFC 3212 ↗, shipped in the 2024 edition ↗. The E0716 transcript and both edition runs are real compiles of the files they name.
Po polsku¶
Domyślna reguła jest wąska: wartość tymczasowa ginie na końcu instrukcji, czyli przy średniku. Wyjątkiem jest let, w którym po prawej stronie stoi tak zwane wyrażenie przedłużające — &coś, &mut coś, literał struktury, krotki albo tablicy zawierający taki operand, a także łańcuch dostępu do pola lub indeksu. Wtedy wartość tymczasowa dostaje ten sam czas życia co zmienna i dożywa końca bloku. Decyzja zapada po kształcie zapisu, a nie po tym, co kod robi — i właśnie dlatego Holder { r: &Noisy("H") } się kompiluje, a hold(&Noisy("W")), które buduje dokładnie to samo, kończy się błędem E0716.
Reguła przestaje być ciekawostką w match. Wartości tymczasowe ze scrutinee żyją przez całe dopasowanie, razem z ramionami — więc match rejestr.lock().unwrap().state() trzyma blokadę także w środku ramion, i wywołanie lock() w ramieniu to zakleszczenie. Ta sama treść w let zwalnia blokadę na średniku. W edycji 2024 zmieniła się połowa tej historii dotycząca if let: wartości tymczasowe ze scrutinee są teraz zwalniane przed blokiem else, a nie po całym if/else jak w edycji 2021.
Ostatnia rzecz, która potrafi zamaskować całą lekcję: jeśli typ nie ma Drop ani wnętrza modyfikowalnego, to &Quiet("Q") w ogóle nie wymaga przedłużania — wartość zostaje wypromowana do anonimowego static, a referencja jest &'static. Eksperyment napisany na zwykłej strukturze danych będzie więc wyglądał tak, jakby przedłużało się wszystko. Dopiero implementacja Drop mówi prawdę o tym, kiedy cokolwiek naprawdę ginie.
Szukaj po polsku: czas życia wartości tymczasowej · przedłużenie czasu życia · zwolnienie na średniku · rust temporary lifetime extension · rust E0716