Intro
In part 3 we talked about how to tell the compiler what the lifetime of a function's result(s) are in relationship to the parameters of the function. In this article we will see how to add lifetimes to structures.
Structural Lifetimes
Sometimes it is desirable to have structures which contain pointers, for example, our Num
tuple that we talked about in Part 1 could be borrowing the string instead of owning it:
struct Num(i32, &String);
...but the compiler will complain since we need to give the borrowed string a lifetime:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:17
To fix this, we declare lifetime(s) using template arguments, and then annotate the &
borrows with the lifetime name. This is very similar to what we did for functions:
struct Num<'a>(i32, &'a String);
Let's try it out using code similar to what we did for Part 1 when we implemented Copy
marker trait:
#[derive(Debug, Copy, Clone)]
struct Num<'a>(i32, &'a String);
fn main() {
let two_str = &String::from("two");
let num = Num(2, two_str);
let even = is_even(num);
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: Num) -> bool {
num.0 % 2 == 0
}
You will notice that since we are borrowing the String
, we can actually implement the Copy
trait!
With this lifetime information, the borrow checker can always ensure the lifetime of the borrowed str out lives the Num
struct. Let's try it out:
#[derive(Debug, Copy, Clone)]
struct Num<'a>(i32, &'a String);
fn main() {
let num;
{
let two_str = &String::from("two");
num = Num(2, two_str);
}
let even = is_even(num);
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: Num) -> bool {
num.0 % 2 == 0
}
error[E0716]: temporary value dropped while borrowed
--> src/main.rs:7:24
The borrow checker observes that two_str
lifetime is shorter than the num
lifetime. Hurray for the borrow checker!
Mutable Structural Borrows
Let's see if we can modify is_even()
so that it has a side-effect of adding " is even" or " is odd" to the end of the Num.1
String field:
#[derive(Debug)]
struct Num<'a>(i32, &'a mut String);
fn main() {
let two_str = &mut String::from("two");
let mut num = Num(2, two_str);
let even = is_even(&mut num);
print!("is_even={even:?} num={num:?}")
}
fn is_even(num: &mut Num) -> bool {
if num.0 % 2 == 0 {
*num.1 = format!("{} is even", num.1);
return true;
} else {
*num.1 = format!("{} is odd", num.1);
return false;
}
}
Some important points:
-
mut
sprinkled all around. In particular:-
Num
declaration transitioned from&'a
to&'a mut
. -
Num
can not implement theCopy
orClone
traits automatically, since it has a mutable borrow. -
two_str
is now a&mut
. - Because
Num
does not implementCopy
, we need to borrow it in the call tois_even()
.
-
- We use the
*
to assign to the mutable reference.
If we modify the above so that two_str
is printed at the end of main, we would get an error, since there can only be a single mutable reference... and the Num
structure "took" that single mutable reference.
Linked Lists
With this new found information, it might be tempting to use this new found information in order to define a single linked list of numbers like this:
struct Node<'a> {
val: i32,
next: Option<&'a mut Node<'a>>
}
Notice that we declare a lifetime, and we tell the borrow checker that the next node in the linked list is a borrowed node with this lifetime... recursively (since the next Node
uses the same lifetime).
...and we can write this code that creates a linked list of 3 elements, then truncates it to 2:
#[derive(Debug)]
struct Node<'a> {
val: i32,
next: Option<&'a mut Node<'a>>
}
fn main() {
let mut third = Node {
val: 3,
next: None,
};
let mut second = Node {
val: 2,
next: Some(&mut third),
};
let mut first = Node {
val: 1,
next: Some(&mut second),
};
println!("3 element LL={first:?}");
(&mut first).next.as_mut().unwrap().next = None;
println!("2 element LL={first:?}");
}
However, this structure will be of limited practical use, since the lifetime of every element in the list must be the same or longer than the lifetime of the actual "owned" list elements. Additionally, the mutual exclusion property of mutable references means we can't even change the last println so it prints second
since the mutable reference to it was transferred to first
's next
field.
Recap Of Structure Lifetimes
- We can use the
<'a>
syntax to declare a "template" lifetime to any structure. - This declaration of lifetime(s) can be used in all the borrowed fields, and in other structures that require a lifetime parameter.
- Mutably borrowing inside of structures is impractical due to the mutual exclusion rules of mutable references. It also disables the ability to implement
Copy
andClone
. - Immutable borrowing inside of structures is reasonable, but approach with caution, since you need these borrowed field lifetimes to be the same as (or longer than) the lifetime of an instance of that structure.
However, before you start .clone()
ing everything into your structures, read Part 5 which discusses a few "escape hatches" that allow us to break all these borrow check rules.
Top comments (0)