rustimmutabilitymutableborrow

Rust immutable borrow followed by mutating code in a same method


here is a simplified version of a Font object I'm currently building for inclusion in an embedded system. Inside the Font, I implement a cache of already rasterized glyphs through a HashMap. The retrieve() method must first check if a specific glyph is already in the cache through the find() method. If not found, it must then build the glyph, put it in the cache and then return it to the caller.

The issue is that the retrieve() method must first do an immutable borrow before modifying the cache if the required glyph was not found. Can't find a way to get it to work properly. Here is the code:

use std::collections::HashMap;
use std::collections::hash_map::RandomState;

type GlyphCharCode = u32;
type GlyphsCache = HashMap<GlyphCharCode, Glyph, RandomState>;

struct Glyph { a: u8 }

struct Font {
  cache: GlyphsCache
}

impl Font {
  fn new() -> Font {
    Font { cache: GlyphsCache::new() }
  }

  fn find(&self, charcode: GlyphCharCode) -> Option<&Glyph> {
    self.cache.get(&charcode)
  }

  fn retrieve(&mut self, charcode: GlyphCharCode) -> Option<&Glyph> {
    let result = self.find(charcode);
    match result {
      Some(glyph) => Some(glyph),
      None => { self.cache.insert(charcode, Glyph { a: 3u8 });
                self.find(charcode) }
    }
  }
}

fn main() {
  let charcode: GlyphCharCode = 3;
  let mut font = Font::new(); 
  if let Some(glyph) = font.retrieve(charcode) {
    println!("Glyph value: {}", glyph.a);
  }
}

and here are the error messages:

error[E0502]: cannot borrow `self.cache` as mutable because it is also borrowed as immutable
  --> src/main.rs:26:17
   |
22 |   fn retrieve(&mut self, charcode: GlyphCharCode) -> Option<&Glyph> {
   |               - let's call the lifetime of this reference `'1`
23 |     let result = self.find(charcode);
   |                  ------------------- immutable borrow occurs here
24 |     match result {
25 |       Some(glyph) => Some(glyph),
   |                      ----------- returning this value requires that `*self` is borrowed for `'1`
26 |       None => { self.cache.insert(charcode, Glyph { a: 3u8 });
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

Solution

  • You can use HashMap's entry API to implement your "borrow or else" pattern.

    fn retrieve(&mut self, charcode: GlyphCharCode) -> &mut Glyph {
      self.cache.entry(charcode).or_insert(Glyph { a: 3u8 })
    }
    

    This does the exact same thing that you were trying to do: "Return if present, or fill it out and return otherwise", but it's built-in to HashMap.

    You might consider using or_insert_with, which takes a closure that is only called if the insertion actually needs to occur, if construction is expensive in your use case.

    fn retrieve(&mut self, charcode: GlyphCharCode) -> &mut Glyph {
      self.cache.entry(charcode).or_insert_with(|| Glyph { a: 3u8 })
    }
    

    Also, if Glyph really is that simple, it should be Copy and we can forgo the whole lifetime issue entirely.

    #[derive(Copy)]
    struct Glyph { a: u8 }
    

    Then find can return by value.

    fn find(&self, charcode: GlyphCharCode) -> Option<Glyph> {
      self.cache.get(&charcode).copied()
    }
    

    No lifetimes needed.