Caching values from an owned member breaks lifetime inference

Hello!

Rust beginner here! I'm learning Rust, and in part of my pet project, I have created a caching object, and I'm trying to understand the error messages the compiler is giving me. I tried trimming it down to a minimal example that still gives the error:

use std::collections::HashMap;

struct UT<'a> {
    x: &'a str,
}

struct Factory {
    a: String
}

impl Factory {
    fn new() -> Self {
        Self {
            a: "ABC".to_string()
        }
    }

    fn get_ut(&self) -> UT {
        // Some expensive operations here
        UT {
            x: &self.a
        }
    }
}

struct Test<'a> {
    f: Factory,
    cache: HashMap<String, UT<'a>>
}

impl<'a> Test<'a> {
    fn get_entry(&mut self, key: String) -> &'a str {
        if !self.cache.contains_key(&key) {
            let u = self.f.get_ut();
            self.cache.insert(key.clone(), u);
        }

        self.cache.get(&key).unwrap().x
    }
}

fn main() {
    let mut t = Test {
        f: Factory::new(),
        cache: HashMap::new()
    };

    t.get_entry("A".to_string());
}

Playground link

The error:

error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  --> src/main.rs:34:28
   |
34 |             let u = self.f.get_ut();
   |                            ^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 32:5...
  --> src/main.rs:32:5
   |
32 | /     fn get_entry(&mut self, key: String) -> &'a str {
33 | |         if !self.cache.contains_key(&key) {
34 | |             let u = self.f.get_ut();
35 | |             self.cache.insert(key.clone(), u);
...  |
38 | |         self.cache.get(&key).unwrap().x
39 | |     }
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/main.rs:34:21
   |
34 |             let u = self.f.get_ut();
   |                     ^^^^^^
note: but, the lifetime must be valid for the lifetime `'a` as defined on the impl at 31:6...
  --> src/main.rs:31:6
   |
31 | impl<'a> Test<'a> {
   |      ^^
note: ...so that the expression is assignable
  --> src/main.rs:35:44
   |
35 |             self.cache.insert(key.clone(), u);
   |                                            ^
   = note: expected  `UT<'a>`
              found  `UT<'_>`

I've spent a few hours trying to think of why the error occurs, and I think it is because f can be moved out at any time and dropped and the UTs will no longer be valid?

Some other questions:

  1. I'm still confused why let u = self.f.get_ut(); can not live past the end of the method in the first conflict? self is borrowed, so I thought u would be valid until self is dropped?

  2. I think what I want is to tie the lifetime of the cache UTs to the factory f but I don't think I can do it without making f a &'a Factory?

  3. And at the end, it says it wants a UT<'a>. Isn't that what it gets? Since the lifetime is tied to the HashMap which is part of Test so the lifetimes should be related?

I tried making f a reference and it works: Playground link but I have no idea why.

My goal is more to understand why this is the wrong way to do it, rather than just getting it working

Please forgive me if these are silly questions.

When you see <'a> on a struct, that means "this struct contains external references, and we should use this lifetime to keep track of them". Such a lifetime is larger than the lifetime of the struct, and references that point to fields in the struct would live for a shorter time than 'a.

Let's take a look at a few lines.

let u = self.f.get_ut();
self.cache.insert(key.clone(), u);

The call to get_u here borrows from self, and the mutable reference we got to self lives for a shorter time than the object, and the object lives for a shorter time than 'a. This means that we have a borrow of self that is shorter than 'a. Since get_ut has the following signature:

fn get_ut<'b>(&'b self) -> UT<'b>

the lifetime we get on UT is the one found on self. Thus the u variable has a lifetime shorter than 'a. But cache has values of type UT<'a>, and UT<'shorter_than_a> is a different type from UT<'a>, so you can't insert it into the hashmap. (UT<'longer_than_a> would also be a different type, but it would be implicitly convertible to UT<'a>, so that would work)

Making f a reference works because it allows us to get a reference with lifetime 'a to the factory, instead of just one with the same lifetime as found on the self parameter. It also fits with the explanation from above — the struct contains references into some external location, in this case a factory variable somewhere else, and things marked with 'a inside the struct all point into that external location.

2 Likes

Thanks for responding!

So to make sure I have it right, in order of shortest to longest lifetimes we have:

  • u
  • self in the get_entry (a reference to a Test object?)
  • the instance of Test (t)
  • 'a

and since lifetime of u < 'a, insert fails because it wants something with a lifetime = 'a (or something > 'a since Rust can cast it to 'a)

And additionally, from

fn get_ut<'b>(&'b self) -> UT<'b>

we can further say the lifetime relationships are:

output (the UT result from get_ut) <= &self (A reference to a Factory in get_ut) <= The actual Factory == t.f == t < 'a

Is this correct?

Since I know the Factory should outlive the UT objects it creates, is there a way to tell the compiler that the HashMap contains things that have references that have lifetimes less than the Factory but still have Test own/contain the Factory? Something like HashMap<String, UT<'lifetime_of_f>>?

Yeah those inequalities are pretty much right. And self is indeed a reference to the Test object. It is not possible to store reference into another field in the same struct, no.

Terrific! Thank you for your help!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.