Search code examples
rustclosureslifetimeborrow-checkertrait-objects

"temporary value dropped while borrowed" with HashMap<&str, &dyn Fn(&str) -> bool>


I have a HashMap of strings to functions. From my understanding the &dyn Fn(&str) -> bool is necessary since I want to use both functions and closures, however I'm getting this compile error:

error[E0716]: temporary value dropped while borrowed
  --> src/test.rs:22:26
   |
22 |     checks.insert("k3", &|i| cached_regex.is_match(i));
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
   |                          |
   |                          creates a temporary which is freed while still in use
...
27 |         match checks.get(kvp[0]) {
   |               ------ borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

Example code:

use regex::Regex;
use std::collections::HashMap;

fn main() {
    assert_eq!(run_checks("k1:test, k3:1234"), true);
    assert_eq!(run_checks("k1:test, k3:12345"), false);
    assert_eq!(run_checks("k1:test, k2:test2"), true);
}

fn specific_check(i: &str) -> bool { i == "test" }

fn run_checks(input: &str) -> bool {
    let cached_regex = Regex::new(r"^\d{4}$").unwrap();

    let mut checks: HashMap<&str, &dyn Fn(&str) -> bool> = HashMap::new();
    checks.insert("k1", &specific_check);
    checks.insert("k2", &|i| i == "test2");

    // Not working
    checks.insert("k3", &|i| cached_regex.is_match(i));

    for kvp_pair in input.split(",") {
        let kvp: Vec<&str> = kvp_pair.trim().split(":").collect();

        match checks.get(kvp[0]) {
            Some(check) => {
                if !check(kvp[1]) {
                    return false;
                }
            }
            None => return false,
        }
    }

    true
}

Solution

  • The most straight-forward way to solve this problem is to Box the dyn Fn(&str) -> bool trait objects. Fixed working example:

    use regex::Regex;
    use std::collections::HashMap;
    
    fn main() {
        assert_eq!(run_checks("k1:test, k3:1234"), true);
        assert_eq!(run_checks("k1:test, k3:12345"), false);
        assert_eq!(run_checks("k1:test, k2:test2"), true);
    }
    
    fn specific_check(i: &str) -> bool { i == "test" }
    
    fn run_checks(input: &str) -> bool {
        let cached_regex = Regex::new(r"^\d{4}$").unwrap();
    
        let mut checks: HashMap<&str, Box<dyn Fn(&str) -> bool>> = HashMap::new();
        checks.insert("k1", Box::new(specific_check));
        checks.insert("k2", Box::new(|i| i == "test2"));
        
        // now works
        checks.insert("k3", Box::new(|i| cached_regex.is_match(i)));
    
        for kvp_pair in input.split(",") {
            let kvp: Vec<&str> = kvp_pair.trim().split(":").collect();
    
            match checks.get(kvp[0]) {
                Some(check) => {
                    if !check(kvp[1]) {
                        return false;
                    }
                }
                None => return false,
            }
        }
    
        true
    }
    

    playground


    Detailed explanation behind solution

    The type annotation in this line is incomplete since the lifetimes of the references and of the trait object itself are omitted:

    let mut checks: HashMap<&str, &dyn Fn(&str) -> bool> = HashMap::new();
    

    Given what keys and values you insert into checks Rust infers the complete type of checks to be HashMap<&'static str, &'static (dyn for<'a> Fn(&'a str) -> bool + 'static)>. The first two inserts match this type signature but the final third one does not. The value used in the third insert has the type &'static (dyn for<'a> Fn(&'a str) -> bool + 'b) where 'b represents the lifetime of the captured cached_regex variable. The reason Rust throws a compiler error is because it's invalid to create a 'static reference to some type with a non-'static lifetime since it would be possible for the reference to become invalidated. Boxing the trait object avoids this issue since we no longer have to create a 'static reference to it, therefore there's no longer the requirement that the trait object itself needs to live for 'static and the final third insert can be safely made.