DEV Community

Michal Ciesielski
Michal Ciesielski

Posted on • Edited on

Lifetimes in Rust - one difficult exercise

The excellent online book Rust By Practice in the Lifetime chapter has a difficult exercise called... A difficult exercise. I struggled with it for a while, then looked at the solution and - I still did not get it. Not until I read the chapter on ownership from The Rustonomicon. Let's dive into the problem and describe the solution in detail.

First we need to go over the code in the exercise and explain what it does. It defines a structure called List; that structure has nothing to do with an actual list, it is just a container for a single object. The object it holds is an instance of a Manager structure. The List gives mutable access to the Manager but not directly - instead it creates container called Interface that holds mutable reference to the Manager. This is done in get_interface method that returns Interface. The only method of Interface is noop(self).

struct Interface<'a> {
    manager: &'a mut Manager<'a>
}

impl<'a> Interface<'a> {
    pub fn noop(self) {
        println!("interface consumed");
    }
}

struct Manager<'a> {
    text: &'a str
}

struct List<'a> {
    manager: Manager<'a>,
}

impl<'a> List<'a> {
    pub fn get_interface(&'a mut self) -> Interface {
        Interface {
            manager: &mut self.manager
        }
    }
}

fn main() {
    let mut list = List {
        manager: Manager {
            text: "hello"
        }
    };

    list.get_interface().noop(); // <--- Interface created and used here.

    println!("Interface should be dropped here and the borrow released");

    use_list(&list);
}

fn use_list(list: &List) {
    println!("{}", list.manager.text);
}
Enter fullscreen mode Exit fullscreen mode

When trying to run this code, rust fails to compile it with the following error:

Compilation error

According to rust's lifetime rules, borrow is valid from it's declaration to it's last use. Last use of Interface is when noop() is called in the main function. It should be similar to this example:

fn main() {
    let mut x = 1;
    let y = &mut x;
    // print!("{x}"); <-- this will not compile, 
    //                    x is mutably borrowed so print cannot borrow it again

    *y = 42; // last use of y

    print!("{x}"); // x no longer borrowed, can be used
}
Enter fullscreen mode Exit fullscreen mode

So why it does not work? Let's analyze reference lifetimes in the main function. I am using similar notation that the The Rustonomicon is using, showing scopes of each lifetime. Note this is not valid rust syntax!

fn main() {
    'a {
        let mut list = List { // list has lifetime 'a
            manager: Manager {
                text: "hello"
            }
        };        

        'b {
            list.get_interface().noop(); // Interface uses lifetime 'b
        }

        // 'b is out of scope
        println!("Interface should be dropped here and the borrow released");

        use_list(&list);
    } // lifetime 'a ends here
}
Enter fullscreen mode Exit fullscreen mode

But... this still does not explain the issue. 'b lifetime is short and ends before use_list is called. To understand what happens we need to look closer at get_interface() method declaration:

impl<'a> List<'a> {
    pub fn get_interface(&'a mut self) -> Interface {
        Interface {
            manager: &mut self.manager // what is lifetime of this?
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Rust lifetime elision allows us to skip some lifetime parameters. If we make all lifetimes explicit, the declaration will look as below

impl<'a> List<'a> {
    pub fn get_interface(&'a mut self) -> Interface<'a> {
        Interface::<'a> {
            manager: &'a mut self.manager
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The lifetime 'a from above snippet will be the same as the lifetime 'a from main function since we are calling this method on list object in main. This means Interface.manager will hold reference with 'a lifetime. And if you look inside Manager struct it uses this lifetime in text reference. Recall the implementation of Interface:

struct Interface<'a> {
    manager: &'a mut Manager<'a>
}

Enter fullscreen mode Exit fullscreen mode

Even if we stop using Interface, the manager reference is still alive as it is using 'a lifetime, which in get_interface will be bound to List's lifetime also called 'a here.

How do we fix this? We need to untangle Interface's lifetime from Manager's lifetime. Let's introduce separate lifetime 'b for Interface's reference. I will consistently use 'a and 'b which should make it easier to track which lifetime is which.

struct Interface<'b, 'a> {
    manager: &'b mut Manager<'a> // reference with lifetime 'b
                                 // holding Manager with lifetime 'a
}

impl<'b, 'a> Interface<'b, 'a> {
    pub fn noop(self) {
        println!("interface consumed");
    }
}

Enter fullscreen mode Exit fullscreen mode

We could bind the lifetimes declaring 'a: 'b to indicate 'a outlives 'b, but this is not necessary in this example.

The last thing remaining is to create Interface instance with proper lifetimes. List uses only one lifetime 'a, but we can extend get_interface method to take another generic lifetime parameter 'b.

impl<'a> List<'a> {
    pub fn get_interface<'b>(&'b mut self) -> Interface<'b, 'a> {
        Interface {
            manager: &mut self.manager
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Now we explicitly told rust to use 'b lifetime for reference inside Interface. Once usage is done, rust can release the reference and list becomes available.

The Rustonomicon describes itself as the awful details that you need to understand when writing Unsafe Rust programs. In this example we did not do any unsafe rust, yet the knowledge from that book was very helpful. I recommend reading The Rustonomicon even if you only plan to play safe.

Sources:

  1. https://doc.rust-lang.org/nomicon/lifetimes.html
  2. https://practice.course.rs/lifetime/advance.html
  3. https://doc.rust-lang.org/nomicon/lifetime-elision.html

Top comments (0)