Lifetimes in Rust 🦀

Anoop P Oommen
6 min readDec 9, 2022

a simple explanation to lifetimes in rust

In Rust, a lifetime is a way to specify the scope for which a reference is valid. In other words, a lifetime is a way to ensure that a reference is not used after the data it points to has been deallocated.

Lifetimes are important in Rust because the language uses a borrowing and mutability model to manage memory and references. This means that references are statically checked at compile time, and the borrow checker ensures that references are always valid and not used after the data they point to has been deallocated.

By using lifetimes, you can specify the scope for which a reference is valid, and the Rust compiler will ensure that the reference is not used outside of that scope. This helps prevent common memory errors, such as null or dangling references, and allows you to write safe and efficient code

The basics

Let’s start with a quick example. Imagine you have two variables, x and y, and you want to create a new variable, z, that contains the sum of x and y. In Rust, you might write something like this

fn main() {
let x = 5;
let y = 10;

let z = x + y;

println!("{}", z);
}

Pretty straightforward, right? But what if you want to pass x and y to a function and have it return the sum? In that case, you might write something like this

fn main() {
let x = 5;
let y = 10;

let z = add(x, y);

println!("{}", z);
}

fn add(a: i32, b: i32) -> i32 {
a + b
}

Again, not too difficult to understand. But now imagine you want to create a function that takes a reference to x and y, and returns a reference to the sum. In other words, you want to avoid copying x and y into the function, and instead work with references to the original variables. In that case, you might write something like this

fn main() {
let x = 5;
let y = 10;

let z = add(&x, &y);

println!("{}", z);
}

fn add(a: &i32, b: &i32) -> &i32 {
&(a + b)
}

But wait! If you try to compile this code, you’ll get an error. The problem is that the return type of add is a reference to the sum of a and b, but a and b are only temporary values that don't actually have a memory address. In other words, the reference returned by add would be pointing to a location in memory that doesn't actually exist.

This is where lifetimes come in. In order to fix this code, we need to tell Rust that the reference returned by add must have the same lifetime as the references passed to it. We do this by adding a lifetime parameter, 'a, to the add function, like this

fn main() {
let x = 5;
let y = 10;

let z = add(&x, &y);

println!("{}", z);
}

fn add<'a>(a: &'a i32, b: &'a i32) -> i32 {
(a + b)
}

Now the add function takes two references with the lifetime 'a, and returns a reference with the same lifetime 'a. This tells Rust that the reference returned by add is valid for the same lifetime as the references passed to it.

So there you have it, folks — that’s the basics of lifetimes in Rust. Just remember: in order to avoid dangling references and other memory-related mishaps, you need to specify the lifetimes of your references. And don’t worry if it doesn’t make sense right away — just keep practicing, and soon you’ll be a lifetime pro!

🦀 The bonus part!

Now that you’ve got the basics of lifetimes in Rust down, it’s time to take your knowledge to the next level. In this section, we’ll explore some advanced uses and features of lifetimes in Rust, including lifetime elision, the 'static lifetime, and the 'a and 'b conventions.

Lifetime elision is a special set of rules that Rust uses to automatically infer the lifetimes of references in function signatures. These rules are based on the idea that the lifetimes of references in a function should be as short as possible. For example, consider the following function

fn foo(x: &i32, y: &i32) -> &i32 {
if x > y {
return x;
} else {
return y;
}
}

In this case, the lifetime of the references passed to foo is the same as the lifetime of the reference returned by foo. Therefore, we can omit the lifetime parameters in the function signature and let Rust infer them automatically, like this

fn foo(x: &i32, y: &i32) -> &i32 {
if x > y {
return x;
} else {
return y;
}
}

Lifetime elision is a powerful feature that can help you write more concise and readable code. However, it’s important to understand that the rules of lifetime elision can be complex, and they may not always infer the lifetimes you want. In those cases, you may need to specify the lifetimes manually.

Another advanced use of lifetimes in Rust is the 'static lifetime. This lifetime represents the lifetime of the entire program. In other words, a reference with the 'static lifetime is valid for the entire duration of the program, from start to finish. This lifetime is useful for references to global variables and other values that exist for the entire duration of the program.

For example, consider the following code

static FOO: i32 = 42;

fn main() {
let x = &FOO;
println!("{}", x);
}

Here, the reference x has the 'static lifetime, because it refers to the global FOO variable, which exists for the entire duration of the program. This means that we can use the 'static lifetime to specify the lifetime of x in the function signature, like this

static FOO: i32 = 42;

fn main() {
let x: &'static i32 = &FOO;
println!("{}", x);
}

inally, it’s worth mentioning the 'a and 'b conventions for naming lifetimes. These conventions are not required by the Rust compiler, but they are widely used in the Rust community to make code more readable and intuitive.

The 'a convention is used to indicate a generic lifetime, which could be any valid lifetime. For example, consider the following code

fn foo<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
if x > y {
return x;
} else {
return y;
}
}

Here, the 'a lifetime parameter indicates that the references `xandy` have the same lifetime, which could be any valid lifetime. This makes the code more readable and intuitive, because it explicitly states that the references have the same lifetime, without needing to specify the exact lifetime.

The 'b convention, on the other hand, is used to indicate a different lifetime from 'a. This is useful when a function takes multiple references with different lifetimes. For example, consider the following code

fn foo<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 {
if x > y {
return x;
} else {
return y;
}
}

Here, the 'a and 'b lifetime parameters indicate that the references x and y have different lifetimes. This makes the code more readable and intuitive, because it explicitly states that the references have different lifetimes, without needing to specify the exact lifetimes.

In conclusion, lifetimes in Rust are a powerful and flexible feature that allows you to write safe and efficient code. By understanding the basics of lifetimes and how to specify them in your code, you can avoid common pitfalls and write code that is both correct and readable. And by exploring advanced uses of lifetimes, such as lifetime elision and the 'static lifetime, you can take your Rust skills to the next level. Happy coding!

--

--

Anoop P Oommen
Anoop P Oommen

No responses yet