Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entry::Occupied.get() returns a value referencing data owned by the current function even though hashmap should have the ownership

Tags:

rust

ownership

My goal was to implement the suggested improvement on the cacher struct of the rust book chapter 13.1, that is creating a struct which takes a function and uses memoization to reduce the number of calls of the given function. To do this, I created a struct with an HashMap

struct Cacher<T, U, V>
where T: Fn(&U) -> V, U: Eq + Hash
{
  calculation: T,
  map: HashMap<U,V>,
}

and two methods, one constructor and one which is resposible of the memoization.

impl<T, U, V> Cacher<T, U, V>
    where T: Fn(&U) -> V, U: Eq + Hash
{
    fn new(calculation: T) -> Cacher<T,U,V> {
        Cacher {
            calculation,
            map: HashMap::new(),
        }
    }

    fn value(&mut self, arg: U) -> &V {
        match self.map.entry(arg){
            Entry::Occupied(occEntry) => occEntry.get(),
            Entry::Vacant(vacEntry) => {
                let argRef = vacEntry.key();
                let result = (self.calculation)(argRef);
                vacEntry.insert(result)
            }
        }
    }
}

I used the Entry enum, because I didn't found a better way of deciding if the HashMap contains a key and - if it doesn't - calculating the value and inserting it into the HashMap as well as returning a reference to it.

If I want to compile the code above, I get an error which says that occEntry is borrowed by it's .get() method (which is fine by me) and that .get() "returns a value referencing data owned by the current function".

My understanding is that the compiler thinks that the value which occEntry.get() is referencing to is owned by the function value(...). But shouldn't I get a reference of the value of type V, which is owned by the HashMap? Is the compiler getting confused because the value is owned by the function and saved as result for a short moment?

let result = (self.calculation)(argRef);
vacEntry.insert(result)

Please note that it is necessary to save the result temporarily because the insert method consumes the key and such argRef is not valid anymore. Also I acknowledge that the signature of value can be problematic (see Mutable borrow from HashMap and lifetime elision) but I tried to avoid a Copy Trait Bound.

For quick reproduction of the problem I append the use statements necessary. Thanks for your help.

use std::collections::HashMap;
use std::cmp::Eq;
use std::hash::Hash;
use std::collections::hash_map::{OccupiedEntry, VacantEntry, Entry};
like image 741
Nico227 Avatar asked Feb 08 '20 17:02

Nico227


1 Answers

Let's take a look at OccupiedEntry::get()'s signature:

 pub fn get(&self) -> &V

What this signature is telling us is that the reference obtained from the OccupiedEntry can only live as long as the OccupiedEntry itself. However, the OccupiedEntry is a local variable, thus it's dropped when the function returns.

What we want is a reference whose lifetime is bound to the HashMap's lifetime. Both Entry and OccupiedEntry have a lifetime parameter ('a), which is linked to the &mut self parameter in HashMap::entry. We need a method on OccupiedEntry that returns a &'a V. There's no such method, but there's one that returns a '&a mut V: into_mut. A mutable reference can be implicitly coerced to a shared reference, so all we need to do to make your method compile is to replace get() with into_mut().

fn value(&mut self, arg: U) -> &V {
    match self.map.entry(arg) {
        Entry::Occupied(occ_entry) => occ_entry.into_mut(),
        Entry::Vacant(vac_entry) => {
            let arg_ref = vac_entry.key();
            let result = (self.calculation)(arg_ref);
            vac_entry.insert(result)
        }
    }
}
like image 132
Francis Gagné Avatar answered Oct 23 '22 11:10

Francis Gagné