0

I have seen several of these questions but couldn't find a solution to my problem yet.

The following caching function in Rust doesn't compile. It produces an error when calling insert:

fn get_or_load_icon<'a>(icon_cache: &'a mut HashMap<String, TextureHandle>, icon_path: &str) -> &'a TextureHandle {
    if let Some(texture_cache) = icon_cache.get(icon_path) {
        texture_cache
    } else {
        let texture = load_image(icon_path);
        icon_cache.insert(icon_path.to_string(), texture);
        icon_cache.get(icon_path).unwrap()
    }
}
error[E0502]: cannot borrow `*icon_cache` as mutable because it is also borrowed as immutable
    --> src\main.rs:1108:3
     |
1103 | fn get_or_load_icon<'a>(icon_cache: &'a mut HashMap<String, TextureHandle>, icon_path: &str) -> &'a TextureHandle {
     |                     -- lifetime `'a` defined here
1104 |     if let Some(texture_cache) = icon_cache.get(icon_path) {
     |                                  ------------------------- immutable borrow occurs here
1105 |         texture_cache
     |         ------------- returning this value requires that `*icon_cache` is borrowed for `'a`
...
1108 |         icon_cache.insert(icon_path.to_string(), texture);
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

I understand that the immutable borrow is icon_cache.get and the mutable borrow is icon_cache.insert, but I don't understand why the immutable borrow is still active at that point, or if the lifetime influences anything. I thought the borrow would be dropped after the if let block and be irrelevant in the else.

It's also important to note that the same exact code outside of a function compiles just fine.

I know two solutions to this issue, neither of which is acceptable for me:

  1. Using entry, which clones icon_path even when it can easily be found in the HashMap from the &str.
fn get_or_load_icon<'a>(icon_cache: &'a mut HashMap<String, TextureHandle>, icon_path: &str) -> &'a TextureHandle {
    icon_cache.entry(icon_path.to_string()).or_insert_with(|| load_image(icon_path))
}
  1. Using contains_key and get, which searches the HashMap twice.
fn get_or_load_icon<'a>(icon_cache: &'a mut HashMap<String, TextureHandle>, icon_path: &str) -> &'a TextureHandle {
    if icon_cache.contains_key(icon_path) {
        icon_cache.get(icon_path).unwrap()
    } else {
        let texture = load_image(icon_path);
        icon_cache.insert(icon_path.to_string(), texture);
        icon_cache.get(icon_path).unwrap()
    }
}

Are there other options that could make this work without cloning icon_path or searching twice? Or am I doing something fundamentally wrong writing this function?

CanisLupus
  • 583
  • 5
  • 15
  • 2
    This is the canonical example of the [limits of non-lexical lifetimes](https://doc.rust-lang.org/nomicon/lifetime-mismatch.html#improperly-reduced-borrows). This will be fixed if the Polonius borrow checker ever gets implemented. If you're fine with using nightly, the unstable `hash_raw_entry` feature can do this (I think, not too too familiar). But I'm not sure if the API is definitely gonna stick around, discusstion seems controversial still: [Playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=08d9f063191151c432efc8957c0a4ff7) – isaactfa Jul 09 '23 at 13:15
  • Thank you for the links and playground! Interesting, I wasn't aware of this limitation. I don't want to rely on Nightly, but good to know that there are options to look out for. Would you say that such a function is not possible in current Rust? (with my given signature and types) – CanisLupus Jul 09 '23 at 15:17
  • Yes, the current `HashMap` API can't handle your use case given the current borrow checker implementation. It's of course impossible to say without benchmarking, but Rust's hashmap implementation is already really fast, so if you use a fast string hasher, like FNV or FxHash (given that you don't need a cryptograhic hash function), your second example solution is probably still going to be really, really performant. – isaactfa Jul 09 '23 at 16:33
  • @CanisLupus No, it's definitely possible with current Rust. It just needs `unsafe`. Luckily, the [`polonius_the_crab`](https://docs.rs/polonius-the-crab) crate provides a sound encapsulation to the `unsafe` code. Like this: https://gist.github.com/rust-play/3a6092e235c55adc350971a1148447cc – Finomnis Jul 09 '23 at 17:31
  • @isaactfa Thanks! I indeed am using the second solution currently. My current use case will not be affected by the small performance loss of the 2 lookups. I'm just sad that this needs to resort to macros or unsafe code, because it's so common. :) – CanisLupus Jul 09 '23 at 18:16
  • @Finomnis Ah, I should have mentioned that I was not looking for unsafe code. I did think of investigating the unsafe side of things but decided to skip it for now. Thanks for the snippet using polonius! – CanisLupus Jul 09 '23 at 18:16

0 Answers0