In Part I we discussed the motivation behind lifetime management in Rust and how it works from a function. In this installment we will explore how lifetime helps us model containment relationship (that is, when an object contains a reference to another object).
The Business Requirement
We will design a Person
type. A person may own a Car
. A person should be able to buy and sell cars. Two persons should be able to exchange (or trade) their cars.
Design the Types
The Car
type is easy. We will keep it simple.
struct Car {
model : String
}
How should the Person
type contain a Car
? A full containment will look like this:
struct Person {
car : Option<Car>
}
This is easy from a memory management point of view. But it has several problems. A car is not an integral part of a person. Buying and trading in cars will create copies of cars. For performance reasons we will like to avoid that. Instead, we will like Person
to store an optional reference to a Car
. But that’s when things begin get complicated.
Containing a Reference Pointer
In Rust a reference must point to a valid memory area. Which means that the container object must not live longer than the object that it holds a reference to. Rust requires us to model this rule using lifetime parameters. Our Person
type will look like this.
struct Person<'a> {
car:Option<&'a Car>
}
In Person<'a>
we are saying that there is a lifitime named a
.
In Option<&'a Car>
we are saying that the Car
object has a lifetime of a
.
After this, the compiler will ensure that the Person
object has a lifetime of a
or less. This will be done to avoid a Person
object from containing an invalid Car
reference pointer.
What we did here is basic common sense. Unfortunately, the Rust compiler does not let us elide the lifetime parameters in this situation. At least not right now.
Write the Implementation
Now that our type is designed, we can go ahead an implement the methods.
impl <'a> Person<'a> {
fn new() -> Person<'a> {
Person{
car: None
}
}
fn buy_car(&mut self, c : &'a Car) {
self.car = Some(c);
}
fn sell_car(&mut self) {
self.car = None;
}
}
This has everything we need except for trading cars between persons. We will develop that later.
As you have caught on by now, lifetime parameters are built into the data type using the same syntax as generics (the full type name is: Person<'a>
). That is why we start the implementation using impl <'a> Person<'a>
just like we would if the Person
type used generics. Technically this lets us write different implementations for different types of lifetimes. But this concept is beyond my understanding for now.
Using the Contained Reference
Alright. Let’s take our code for a spin. This will compile just fine.
fn main() {
let car = Car{model: "Honda Civic".to_string()};
let mut bob = Person::new();
bob.buy_car(&car);
println!("{}", bob.car.unwrap().model);
}
The order in which you create bob
and car
does not matter. This will also work.
fn main() {
let mut bob = Person::new();
let car = Car{model: "Honda Civic".to_string()};
bob.buy_car(&car); //OK!
println!("{}", bob.car.unwrap().model);
}
In the old days this would have caused compile error because compiler thought the car
got destroyed before bob
. We can try to enforce that order using a scope. Now the code will run afoul.
fn main() {
let mut bob = Person::new();
{
let car = Car{model: "Honda Civic".to_string()};
bob.buy_car(&car); //Error!
}
println!("{}", bob.car.unwrap().model);
}
The Car
object does not live as long as the Person
. This violates our lifetime rule.
The compiler is good but there are cases where it can not deterministically asses lifetime and errs on the side of safety. Consider the example below.
fn main() {
let ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();
{
let civic = Car{model: "Honda Civic".to_string()};
bob.buy_car(&civic); //Error!
bob.buy_car(&ghibli);
}
println!("{}", bob.car.unwrap().model);
}
The code does not compile because the civic
variable doesn’t live as long as bob
. This is unfortunate because clearly the code is perfectly safe from a human developer’s point of view. At the end of the inner scope bob
no longer has any reference to civic
. The system is strictly going by the way we declared the buy_car(&mut self, c : &'a Car)
method. All it knows is that the supplied Car
has a lifetime of a
and the container object must not outlive it.
Recall, we can have multiple immutable borrowers as long as there are no other mutable borrowers. This makes it possible for two people to own the same car at the same time.
fn main() {
let mut bob = Person::new();
let mut alice = Person::new();
let civic = Car{model: "Honda Civic".to_string()};
bob.buy_car(&civic);
alice.buy_car(&civic);
println!("Bob has: {}", bob.car.unwrap().model);
println!("Alice has: {}", alice.car.unwrap().model);
}
Having two owners of a car is a possible violation of our nation’s legal construct. Compiler should not enforce such rules. If you must lean on the compiler to deny sharing of a car then read on. We discuss that case later in this article.
Implement Trading
Trading will involve swapping cars between two Person
instances. We add this method to the implementation:
impl <'a> Person<'a> {
//Other methods omitted...
fn trade_with(&mut self, other : &mut Person<'a>) {
let tmp = other.car;
other.car = self.car;
self.car = tmp;
}
}
Nothing special here. Except to note that the lifetime is a part of the data type. The other
variable has a full data type of &mut Person<'a>
.
We can now use the trade_with()
method like this.
fn main() {
let civic = Car{model: "Honda Civic".to_string()};
let ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();
let mut alice = Person::new();
bob.buy_car(&civic);
alice.buy_car(&ghibli);
bob.trade_with(&mut alice);
println!("Bob has: {}", bob.car.unwrap().model);
println!("Alice has: {}", alice.car.unwrap().model);
}
These days the order in which Car
and Person
objects are created makes no difference. The compiler is smart enough to deduce their lifetimes.
You can throw a really devious case at the compiler. It will catch the problem and won’t compile.
fn main() {
let mut bob = Person::new();
let civic = Car{model: "Honda Civic".to_string()};
{
let ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut alice = Person::new();
bob.buy_car(&civic);
alice.buy_car(&ghibli); //Error!
bob.trade_with(&mut alice);
println!("Alice has: {}", alice.car.unwrap().model);
}
println!("Bob has: {}", bob.car.unwrap().model);
}
The compiler correctly deduces that bob
becomes the new owner of the Maserati Ghibli and Ghibli doesn’t live as long as bob
does.
Borrow Rules Apply
When an object holds a reference to another it borrows the reference and all standard borrowing rules apply. This is expected. I mention them here just to reinforce the concepts.
You can have as many immutable borrows as you want as long as there are no mutable borrows.
let ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();
bob.buy_car(&ghibli); //bob borrows ghibli immutably
let p1 = &ghibli; //More immutable borrows are OK
let p2 = &ghibli;
A mutable borrow is exclusive and is only permitted if there is no other borrow of any kind.
let mut ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();
bob.buy_car(&ghibli); //bob borrows ghibli immutably
let p1 = &mut ghibli; //Can't do this.
You can not move an object while someone has borrowed it’s reference. Because doing so will make the borrowed reference point to an invalid memory area.
let mut ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();
bob.buy_car(&ghibli); //bob borrows ghibli
let g = ghibli; //Can't move
This is, however, somewhat unexpected.
let civic = Car{model: "Honda Civic".to_string()};
let mut ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();
bob.buy_car(&ghibli);
bob.buy_car(&civic);
let p1 = &mut ghibli; //Still Can't do this
From a human developer’s point of view the code above is safe. bob
no longer borrows any reference to ghibli
at a time when we are trying to borrow ghibli
mutably. But the compiler is unable to deduce that.
Prevent Sharing of a Reference
Earlier we have shown that two Person
objects can share the same Car
reference. In some situations this may not be desired. For example this prevents a Person
from tinkering with (mutating) the car in any way. We can solve this issue by giving a Person
a mutable reference to a Car
. Remember a mutable reference creates an exclusive access. Nobody else can have any kind of reference to the same object. Here is the full code.
struct Car {
model : String
}
struct Person<'a> {
//Hold a mutable reference
car:Option<&'a mut Car>
}
impl <'a> Person<'a> {
fn new() -> Person<'a> {
Person{
car: None
}
}
fn buy_car(&mut self, c : &'a mut Car) {
self.car = Some(c);
}
fn sell_car(&mut self) {
self.car = None;
}
fn trade_with<'b>(&mut self, other : &'b mut Person<'a>) {
let tmp = other.car.take();
other.car = self.car.take();
self.car = tmp;
}
}
fn main() {
let mut civic = Car{model: "Honda Civic".to_string()};
let mut ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();
let mut alice = Person::new();
bob.buy_car(&mut civic);
alice.buy_car(&mut ghibli);
bob.trade_with(&mut alice);
println!("Bob has: {}", bob.car.unwrap().model);
println!("Alice has: {}", alice.car.unwrap().model);
}
That will work fine. Notice in the trade_with()
method we had to introduce a new lifetime for the other
variable. The reasoning gets complicated. Because the cars are switching hands they may not outlive either one of the persons. Compiler needs a little bit of help here. We are now saying that other
may have a different lifetime than &self
.
Sharing is now disallowed.
fn main() {
let mut civic = Car{model: "Honda Civic".to_string()};
let mut bob = Person::new();
let mut alice = Person::new();
bob.buy_car(&mut civic);
alice.buy_car(&mut civic); //Error!
println!("Bob has: {}", bob.car.unwrap().model);
println!("Alice has: {}", alice.car.unwrap().model);
}
Not All is Well
Well, as we have seen above it’s not that hard to model containment of reference pointers. The bad news is that this model does not always work. Consider this function. It will not compile.
fn shop_for_car(p : &mut Person) {
let car = Car{model: "Mercedes GLK350".to_string()};
p.buy_car(&car); //Error! car doesn't live long enough
}
That is because the car
object simply doesn’t live as long as the Person
buying it. So how can we keep reference to an object that is created in an inner scope like a function? The answer lies in heap allocation which in Rust is achieved via Box::new
. We will explore that in Part III.