Complex lifetime scenarios and elision rules
Lifetimes are Rust's way of tracking how long references are valid. They prevent dangling pointers and use-after-free bugs at compile time with zero runtime cost.
fn dangling() -> &String { // ERROR: Missing lifetime
let s = String::from("hello");
&s // ERROR: Returns reference to local variable
} // s dropped here, reference would be invalid!
Lifetime annotations ('a, 'b, etc.) are generic parameters for references, telling the compiler how long they're valid.
Rust has three rules that let you omit lifetime annotations in common cases:
// What you write:
fn first_word(s: &str) -> &str
// What compiler sees:
fn first_word<'a>(s: &'a str) -> &str
// What you write:
fn first_word(s: &str) -> &str
// What compiler infers:
fn first_word<'a>(s: &'a str) -> &'a str
// What you write:
impl Parser {
fn parse(&self, input: &str) -> &str
}
// What compiler infers:
impl<'a> Parser<'a> {
fn parse(&'a self, input: &str) -> &'a str
}
// ERROR: Multiple inputs, ambiguous output
fn longest(x: &str, y: &str) -> &str { // Which lifetime?
if x.len() > y.len() { x } else { y }
}
// FIX: Explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
/// HTTP request parser that borrows from the input buffer
/// No allocations, zero-copy - all parsing happens via slices
pub struct HttpRequest<'buf> {
// All these reference the original buffer
method: &'buf str,
path: &'buf str,
version: &'buf str,
headers: Vec<Header<'buf>>,
body: Option<&'buf [u8]>,
}
pub struct Header<'buf> {
name: &'buf str,
value: &'buf str,
}
impl<'buf> HttpRequest<'buf> {
/// Parse HTTP request from buffer
/// Lifetime 'buf means all returned references point into buffer
pub fn parse(buffer: &'buf str) -> Result<Self, ParseError> {
let mut lines = buffer.lines();
// Parse request line: "GET /path HTTP/1.1"
let request_line = lines.next().ok_or(ParseError::Empty)?;
let mut parts = request_line.split_whitespace();
let method = parts.next().ok_or(ParseError::InvalidMethod)?;
let path = parts.next().ok_or(ParseError::InvalidPath)?;
let version = parts.next().ok_or(ParseError::InvalidVersion)?;
// Parse headers
let mut headers = Vec::new();
for line in lines {
if line.is_empty() {
break; // End of headers
}
if let Some((name, value)) = line.split_once(':') {
headers.push(Header {
name: name.trim(),
value: value.trim(),
});
}
}
Ok(HttpRequest {
method,
path,
version,
headers,
body: None, // Simplified
})
}
/// Get header value by name
/// Returns borrowed reference - no allocation
pub fn header(&self, name: &str) -> Option<&'buf str> {
self.headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case(name))
.map(|h| h.value)
}
/// Get path segments as slices
/// All slices point into original buffer
pub fn path_segments(&self) -> impl Iterator<Item = &'buf str> {
self.path.split('/').filter(|s| !s.is_empty())
}
}
#[derive(Debug)]
pub enum ParseError {
Empty,
InvalidMethod,
InvalidPath,
InvalidVersion,
}
// Usage example
fn parse_http_request_example() {
let buffer = "GET /api/users/123 HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Type: application/json\r\n\
\r\n";
match HttpRequest::parse(buffer) {
Ok(req) => {
println!("Method: {}", req.method);
println!("Path: {}", req.path);
if let Some(host) = req.header("Host") {
println!("Host: {}", host);
}
// Path segments - all zero-copy
for segment in req.path_segments() {
println!("Segment: {}", segment);
}
}
Err(e) => eprintln!("Parse error: {:?}", e),
}
}
| Approach | Allocations | Speed | Memory |
|----------|-------------|-------|---------|
| String-copying parser | ~10-20 | 1x | High |
| Zero-copy with lifetimes | 0 | 10-50x | Minimal |
/// SQL query builder that borrows table and column names
/// Prevents copying strings during query construction
pub struct QueryBuilder<'ctx> {
table: &'ctx str,
columns: Vec<&'ctx str>,
conditions: Vec<Condition<'ctx>>,
limit: Option<usize>,
}
pub struct Condition<'ctx> {
column: &'ctx str,
operator: &'ctx str,
value: QueryValue<'ctx>,
}
pub enum QueryValue<'ctx> {
String(&'ctx str),
Int(i64),
Null,
}
impl<'ctx> QueryBuilder<'ctx> {
pub fn new(table: &'ctx str) -> Self {
Self {
table,
columns: Vec::new(),
conditions: Vec::new(),
limit: None,
}
}
/// Select specific columns
pub fn select(&mut self, columns: &[&'ctx str]) -> &mut Self {
self.columns.extend_from_slice(columns);
self
}
/// Add WHERE condition
pub fn where_eq(&mut self, column: &'ctx str, value: &'ctx str) -> &mut Self {
self.conditions.push(Condition {
column,
operator: "=",
value: QueryValue::String(value),
});
self
}
/// Add LIMIT clause
pub fn limit(&mut self, n: usize) -> &mut Self {
self.limit = Some(n);
self
}
/// Build SQL query string
/// Note: This DOES allocate because we're building a new String
/// But column/table names don't get copied
pub fn build(&self) -> String {
let mut query = String::from("SELECT ");
// Columns
if self.columns.is_empty() {
query.push('*');
} else {
query.push_str(&self.columns.join(", "));
}
// FROM clause
query.push_str(" FROM ");
query.push_str(self.table);
// WHERE clause
if !self.conditions.is_empty() {
query.push_str(" WHERE ");
for (i, cond) in self.conditions.iter().enumerate() {
if i > 0 {
query.push_str(" AND ");
}
query.push_str(cond.column);
query.push(' ');
query.push_str(cond.operator);
query.push(' ');
match &cond.value {
QueryValue::String(s) => {
query.push('\'');
query.push_str(s);
query.push('\'');
}
QueryValue::Int(n) => query.push_str(&n.to_string()),
QueryValue::Null => query.push_str("NULL"),
}
}
}
// LIMIT clause
if let Some(n) = self.limit {
query.push_str(" LIMIT ");
query.push_str(&n.to_string());
}
query
}
}
// Usage
fn query_builder_example() {
// Schema definition (compile-time constants)
const TABLE_USERS: &str = "users";
const COL_ID: &str = "id";
const COL_NAME: &str = "name";
const COL_EMAIL: &str = "email";
let mut query = QueryBuilder::new(TABLE_USERS);
query
.select(&[COL_ID, COL_NAME, COL_EMAIL])
.where_eq(COL_NAME, "Alice")
.limit(10);
let sql = query.build();
println!("Generated SQL: {}", sql);
// Output: SELECT id, name, email FROM users WHERE name = 'Alice' LIMIT 10
}
When different inputs have different lifetimes:
/// Config loader that references both file path and default values
struct ConfigLoader<'path, 'defaults> {
config_path: &'path str,
defaults: &'defaults HashMap<String, String>,
}
impl<'path, 'defaults> ConfigLoader<'path, 'defaults> {
fn new(
config_path: &'path str,
defaults: &'defaults HashMap<String, String>,
) -> Self {
Self {
config_path,
defaults,
}
}
/// Get value from config, falling back to defaults
/// Output lifetime depends on which source it comes from
fn get<'a>(
&'a self,
key: &str,
loaded_config: &'a HashMap<String, String>,
) -> Option<&'a str>
where
'defaults: 'a, // 'defaults must outlive 'a
{
loaded_config
.get(key)
.map(|s| s.as_str())
.or_else(|| self.defaults.get(key).map(|s| s.as_str()))
}
}
fn complicated<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a, // 'b must outlive 'a
{
x // Can only return 'a, not 'b
}
Read as: "'b outlives 'a" or "'b is at least as long as 'a"
use std::collections::HashMap;
/// JWT token parts borrowing from original token string
pub struct JwtToken<'token> {
header: &'token str,
payload: &'token str,
signature: &'token str,
// Parsed claims reference payload
claims: HashMap<&'token str, &'token str>,
}
impl<'token> JwtToken<'token> {
/// Parse JWT token: "header.payload.signature"
/// Zero-copy parsing - all parts are slices of input
pub fn parse(token: &'token str) -> Result<Self, &'static str> {
let mut parts = token.split('.');
let header = parts.next().ok_or("Missing header")?;
let payload = parts.next().ok_or("Missing payload")?;
let signature = parts.next().ok_or("Missing signature")?;
// Parse payload claims (simplified)
let claims = Self::parse_claims(payload)?;
Ok(JwtToken {
header,
payload,
signature,
claims,
})
}
fn parse_claims(payload: &'token str) -> Result<HashMap<&'token str, &'token str>, &'static str> {
// In real code: base64 decode + JSON parse
// Here: simplified key=value parsing
let mut claims = HashMap::new();
for pair in payload.split('&') {
if let Some((key, value)) = pair.split_once('=') {
claims.insert(key, value);
}
}
Ok(claims)
}
/// Get claim value
pub fn get_claim(&self, key: &str) -> Option<&'token str> {
self.claims.get(key).copied()
}
/// Validate token expiration
pub fn is_expired(&self, current_time: u64) -> bool {
if let Some(exp) = self.get_claim("exp") {
if let Ok(exp_time) = exp.parse::<u64>() {
return current_time > exp_time;
}
}
true // No expiration = expired
}
/// Check if token has required scope
pub fn has_scope(&self, required_scope: &str) -> bool {
if let Some(scopes) = self.get_claim("scope") {
return scopes.contains(required_scope);
}
false
}
}
// Usage in API endpoint
fn validate_request(token_str: &str, current_time: u64) -> Result<(), &'static str> {
let token = JwtToken::parse(token_str)?;
if token.is_expired(current_time) {
return Err("Token expired");
}
if !token.has_scope("read:users") {
return Err("Insufficient permissions");
}
Ok(())
}
/// Iterator that borrows from a collection
struct MyIter<'a, T> {
items: &'a [T],
index: usize,
}
impl<'a, T> MyIter<'a, T> {
fn new(items: &'a [T]) -> Self {
Self { items, index: 0 }
}
}
impl<'a, T> Iterator for MyIter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.items.len() {
let item = &self.items[self.index];
self.index += 1;
Some(item)
} else {
None
}
}
}
// Struct with lifetime parameter
struct Container<'a, T> {
value: &'a T,
}
// Implementation requires repeating lifetime
impl<'a, T> Container<'a, T> {
fn new(value: &'a T) -> Self {
Self { value }
}
fn get(&self) -> &'a T { // Returns same lifetime as input
self.value
}
}
// String literals have 'static lifetime
let s: &'static str = "Hello, world!";
// Static variables have 'static lifetime
static GLOBAL_CONFIG: &str = "production";
// Can accept any lifetime, including 'static
fn print_ref<'a>(s: &'a str) {
println!("{}", s);
}
print_ref("static string"); // 'static is compatible with any 'a
'static means:
// ERROR: Returns reference to dropped value
fn create_string() -> &str {
let s = String::from("hello");
&s // ERROR: s dropped at end of function
}
// FIX: Return owned value
fn create_string() -> String {
String::from("hello")
}
// BAD: 'static is too restrictive
fn process(data: &'static str) {
println!("{}", data);
}
// Can only accept string literals or statics!
process("literal"); // OK
let s = String::from("owned");
process(&s); // ERROR: &s is not 'static
// GOOD: Accept any lifetime
fn process(data: &str) {
println!("{}", data);
}
// BAD: Overspecified lifetimes
fn first<'a, 'b, 'c>(x: &'a str, y: &'b str) -> &'c str
where
'a: 'c,
'b: 'c,
{
x
}
// GOOD: Let elision work
fn first<'a>(x: &'a str, y: &str) -> &'a str {
x
}
&self: Output inherits self's lifetime automatically// Elision works
fn first_word(s: &str) -> &str { ... }
// Elision works
impl Parser {
fn parse(&self, input: &str) -> &Token { ... }
}
// Elision fails - need explicit
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
// Elision fails - need explicit
struct Tokenizer<'a> {
input: &'a str,
}
Create a zero-copy string splitter that returns slices without allocating.
Hints:&'a strParse INI-style config where keys and values are slices of the input.
Hints:HashMap<&'a str, &'a str>Build a URL router that stores path patterns as references.
Hints:Uses lifetimes extensively for zero-copy JSON parsing.
View on GitHubZero-copy parser framework built entirely on lifetimes.
View on GitHubHTTP header parsing uses lifetimes to avoid allocations.
View on GitHubRun this code in the official Rust Playground