Search code examples
rusthashmaplifetime

Rust HashMap supporting pointer stability to allow key referencing its value


In Rust, using a HashMap where the key is a &str that refers to a String member within its corresponding value is not directly possible. The following example illustrates the idea:

type PersonMap = HashMap<&str, Person>;

struct Person {
    name: String,
}

This is not feasible due to Rust's ownership and borrowing rules and HashMap's resizing, as explained in this answer (https://stackoverflow.com/a/61021022/84540).

However, I wonder if there exists an unordered map implementation that supports pointer stability (What is pointer stability?), which could make such usage possible. Or is it not possible due to Rust's lifetime checking? Thanks.


Solution

  • You do not need pointer stability to support such a mapping, even without cloning keys. The trick is to use HashSet instead with a wrapper that makes its value behave like its key.

    Here's how that could look:

    use std::borrow::Borrow;
    use std::hash::{Hash, Hasher};
    use std::collections::HashSet;
    
    #[derive(Debug)]
    struct Person {
        name: String,
        // other fields
    }
    
    #[derive(Debug)]
    struct PersonByName(Person);
    
    impl PartialEq for PersonByName {
        fn eq(&self, other: &Self) -> bool {
            self.0.name == other.0.name
        }
    }
    
    impl Eq for PersonByName {}
    
    impl Hash for PersonByName {
        fn hash<H: Hasher>(&self, state: &mut H) {
            self.0.name.hash(state);
        }
    }
    
    impl Borrow<str> for PersonByName {
        fn borrow(&self) -> &str {
            &self.0.name
        }
    }
    
    fn main() {
        let people = vec![
            Person { name: "Jimmy".to_owned() },
            Person { name: "Timmy".to_owned() },
            Person { name: "Lenny".to_owned() },
        ];
        
        let map: HashSet<PersonByName> = people.into_iter().map(PersonByName).collect();
        
        println!("{:?}", map.get("Timmy"));
    }
    
    Some(PersonByName(Person { name: "Timmy" }))
    

    Playground link

    So even though its a "set" and not a "map", we can treat it like a map by making use Rust's Borrow trait when .get-ing a value. The Hash and PartialEq/Eq are there to make it behave like just a string so the HashSet lookup is consistent.