DEV Community

Brian Maher
Brian Maher

Posted on • Updated on

Rust Borrow Checker Explained Part 4: Structure Lifetimes

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);
Enter fullscreen mode Exit fullscreen mode

...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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Try it!

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
}
Enter fullscreen mode Exit fullscreen mode

Try it!

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:7:24
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Try it!

Some important points:

  • mut sprinkled all around. In particular:
    • Num declaration transitioned from &'a to &'a mut.
    • Num can not implement the Copy or Clone traits automatically, since it has a mutable borrow.
    • two_str is now a &mut.
    • Because Num does not implement Copy, we need to borrow it in the call to is_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>>
}
Enter fullscreen mode Exit fullscreen mode

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:?}");
}
Enter fullscreen mode Exit fullscreen mode

Try it!

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 and Clone.
  • 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)