A lifetime annotation describes how long a reference must remain valid in relation to other references. It does not keep the referenced value alive. If the data would be destroyed too soon, the compiler rejects the program; adding an annotation cannot postpone that destruction.
Describe the relationship between inputs and output
Consider a function that returns one of two borrowed strings:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let outer = String::from("a long string");
{
let inner = String::from("short");
let result = longest(&outer, &inner);
assert_eq!(result, "a long string");
}
println!("{outer}");
}
Both inputs must be valid for the lifetime used by the result. They do not
need to have been created together or be destroyed together. Here the caller
uses the result while both strings are alive, even though outer outlives
inner. The function compares byte lengths and chooses a on a tie.
The shared 'a allows the function to return either input. At a call site,
the compiler checks that the inputs can support all required uses of the
result. It need not choose the largest possible lifetime or make the two
owned strings live equally long.
If you instead store the result outside the inner block and use it after
inner is dropped, the call cannot satisfy that contract. This remains true
even when you know these particular contents make outer win. The caller is
checked against the function's signature, which allows either input to be
returned.
Read an outlives bound
The bound 'a: 'b means that 'a outlives 'b. For shared references, a
reference valid for 'a can be used for the shorter requirement 'b:
fn shorten<'a: 'b, 'b>(text: &'a str) -> &'b str {
text
}
fn main() {
let owned = String::from("hello");
assert_eq!(shorten(&owned), "hello");
}
This does not shorten the life of owned. It limits what the returned
reference promises. The reverse conversion is not generally possible: a
reference valid for a shorter borrow cannot promise validity beyond it.
A useful model is a region of program points where a reference must be valid. In that model, an outlives relationship is containment: the longer region includes the shorter one. This helps explain reference lifetimes, but it is not a complete recipe for predicting every borrow-checker decision. Type relationships, aliasing restrictions and control flow also matter.
Shared references are covariant in their lifetime, which permits this shortening. Do not extend that statement to arbitrary transformations of nested types or mutable reference contents; those have their own variance rules.
A borrow can end before its variable leaves scope
fn main() {
let mut values = vec![1, 2, 3];
let first = &values[0];
assert_eq!(*first, 1);
values.push(4);
assert_eq!(values, [1, 2, 3, 4]);
}
first is still in lexical scope at the push, but it is no longer used.
Non-lexical lifetimes let the compiler end this shared borrow before the
mutation. The vector still owns its elements throughout; ending a borrow
does not destroy the borrowed data.
Add another use of first after push and the program is rejected. The
shared borrow would then overlap the mutation, which could reallocate the
vector's storage. Last use explains this example, but it is not a universal
rule that every borrow ends at the last visibly written reference access:
destructors and references stored in other values can also require validity.
Returning a reference requires a valid source
With one borrowed input, lifetime elision can express the relationship for you:
fn first_line(text: &str) -> &str {
text.lines().next().unwrap_or("")
}
fn main() {
let text = String::from("first\nsecond");
assert_eq!(first_line(&text), "first");
let saved = {
let temporary = String::from("keep this\nnext");
first_line(&temporary).to_owned()
};
assert_eq!(saved, "keep this");
}
The first result borrows from text. The second call copies the selected
line into an owned String, so saved remains usable after temporary is
dropped. Another option is to keep the source string alive for as long as
its borrowed result is needed. Choose based on who should own the data.
Changing first_line to return &'static str would not extend its input's
lifetime. Its body can return a slice of an arbitrary caller-owned string,
so that stronger promise is rejected. Returning a string literal can satisfy
a static-reference contract, but it would be a different operation.
What the compiler checks
Lifetime parameters do not add runtime counters or expiration checks to
references. Their relationships are checked during compilation. This does
not promise byte-identical machine code for every pair of differently
annotated functions, or mean all safe Rust abstractions have no runtime
checks: RefCell, for example, checks borrowing dynamically.
Successful output tests cannot establish that a borrowed API accepts the intended callers. Compile examples that should be accepted, and check that examples which would outlive their data are rejected. Both are useful when designing a function that returns references.
For the language rules behind these examples, see the Rust Reference on lifetime elision and lifetime bounds.