Lifetime in Rust – Part 1

Reading Time: 4 minutes

Rust can be a tricky language to work with and one of the prime reasons for that is the way it manages its memory. In my previous blogs, Ownership, and References I discussed how Rust works its memory management and how it manages to refer to a variable without actually taking the ownership of the variable. Now, let’s take a step further. What happens when we refer to something that doesn’t exist?

Languages like C and C++ are filled with instances of memory bugs such as a Dangling reference. Now a dangling reference is a reference variable which points to an invalid memory location. Take a look at the following code:

#[derive(Debug)]
struct Person {
    name: String,
    age: i32,
}

fn main() {
    let person: &Person;
    {
        let ayush: Person = Person {
        name: String::from("Ayush Prashar"),
        age: 25,
        };
    person = &ayush;
    }
println!("{:?}",person.name);
}

Here we declare a structure called Person. Inside the main function, we try to create a reference variable that can hold references to the type person. Now as you can see, inside another scope we create an instance of Person who’s value is held by a variable called ayush. Now we assign a reference to that to the variable person and try to access person outside the scope. Hence in this particular case,  we happen to refer to a memory location that’ll be dropped while the reference holder is still valid. This here is a classic case of a dangling reference. When compiled the compiler states the following error.

Screenshot from 2019-04-01 02-14-28

It’s very clear why this error occurred. The compiler states that the subject of reference doesn’t live long enough. But how did the compiler get to know this?

Borrow Checker

Rust compiler comprises of what is known as a borrow checker. The job of the borrow checker is to check the lifetimes of the reference variable and the subject of reference and ensure if the subject of reference lives at least as long enough as the reference holder. If not, it alarms the compiler which flags an error.

Things are pretty evident while dealing with concrete instances such as above. A value either lives long enough or it doesn’t, there’s no middle-ground. But what if we don’t have any concrete values? What if all we have is a function definition? Have a look at the code below.

fn get_older_person(first_person: &Person, second_person: &Person)
-> &Person {	
	if first_person.age > second_person.age {	
		first_person	
	}
	else {	
		second_person	
	}
}

Here we have a completely valid function which takes in two references referring to a person each and returns a reference to the older one. Now despite being a valid function, it gives us a compile-time error.

Screenshot from 2019-04-01 02-28-16

Now the compiler can’t tell if the value is borrowed from the first parameter or the second. Why is that an issue? To be clear this isn’t the case of a type mismatch where rust can’t tell the datatype of the returned value. Once observed closely, you can see a relation between the parameters and the returned value. The function returns a reference and that reference is either the first parameter or the second but it can not be anything else thanks to the rules of ownership. Any variable created inside the scope will get dropped at the end of the scope and hence the reference to that variable will be a memory bug. Which leave us with the conclusion that a reference returned is always one of the input parameter references.

Hence knowing whether the value returned is always valid we need to establish a period within which all the input references are valid. This demand marks the establishment of Lifetime annotations in Rust. This period is what is denoted by a lifetime annotation.

Lifetime Annotation

A lifetime annotation is depicted by an apostrophe followed by a single literal by convention. It denotes the minimum period for which all the annotated fields are valid. As this period needs to be common between all references, this is always equal to the scope of the shortest valid lifetime. Let’s have a look at our code after the application of the lifetime annotation.

fn get_older_person<'a> (first_person: &'a Person, 
second_person: &'a Person) -> &'a Person {
    if first_person.age > second_person.age { 
        first_person 
    }
    else { 
        second_person 
    }
}

Here the annotation states that the parameters and returned values will all be satisfied for a lifetime common to all of them. Hence the returned value will always be assigned the lifetime of the shortest reference. Now all this would compile easily.

Next, we need to take a deeper look at lifetimes and its use cases. That would follow in part 2 of this blog series. Stay tuned and please provide your valuable feedback.


Knoldus-blog-footer-image

Written by 

Ayush Prashar is a software consultant having more than 1 year of experience. He is familiar with programming languages such as Java, Scala, C, C++ and he is currently working on reactive technologies like Lagom, Akka, Spark, and Kafka. His hobbies include playing table tennis, basketball, watching TV series and football.

Knoldus Pune Careers - Hiring Freshers

Get a head start on your career at Knoldus. Join us!