1

The following is a snippet of a more complicated code, the idea is loading a SQL table and setting a hashmap with one of the table struct fields as the key and keeping the structure as the value (implementation details are not important since the code works fine if I clone the String, however, the Strings in the DB can be arbitrarily long and cloning can be expensive).

The following code will fail with

error[E0382]: use of partially moved value: `foo`
  --> src/main.rs:24:35
   |
24 |         foo_hashmap.insert(foo.a, foo);
   |                            -----  ^^^ value used here after partial move
   |                            |
   |                            value partially moved here
   |
   = note: partial move occurs because `foo.a` has type `String`, which does not implement the `Copy` trait

For more information about this error, try `rustc --explain E0382`.
use std::collections::HashMap;

struct Foo {
    a: String,
    b: String,
}

fn main() {
    let foo_1 = Foo {
        a: "bar".to_string(),
        b: "bar".to_string(),
    };

    let foo_2 = Foo {
        a: "bar".to_string(),
        b: "bar".to_string(),
    };

    let foo_vec = vec![foo_1, foo_2];

    let mut foo_hashmap = HashMap::new();

    foo_vec.into_iter().for_each(|foo| {
        foo_hashmap.insert(foo.a, foo);  // foo.a.clone() will make this compile
    });
}

The struct Foo cannot implement Copy since its fields are String. I tried wrapping foo.a with Rc::new(RefCell::new()) but later went down the pitfall of missing the trait Hash for RefCell<String>, so currently I'm not certain in either using something else for the struct fields (will Cow work?), or to handle that logic within the for_each loop.

Corfucinas
  • 353
  • 2
  • 11
  • I've hit this exact issue before. It's a bit of a mess. Personally, I just take the hit and `clone` the key, to cut out that dependency altogether. Minor performance hit but you can be confident your code is correct. – Silvio Mayolo Jun 27 '22 at 18:19
  • This can't work, because as soon as you have an immutable borrow against a value in the hashmap, you can never mutably borrow the hashmap again -- otherwise you could cause a reallocation that invalidates the reference. – cdhowie Jun 27 '22 at 18:28

1 Answers1

4

There are at least two problems here: First, the resulting HashMap<K, V> would be a self-referential struct, as the K borrows V; there are many questions and answers on SA about the pitfalls of this. Second, even if you could construct such a HashMap, you'd easily break the guarantees provided by HashMap, which allows you to modify V while assuming that K always stays constant: There is no way to get a &mut K for a HashMap, but you can get a &mut V; if K is actually a &V, one could easily modify K through V (by ways of mutating Foo.a ) and break the map.

One possibility is to change Foo.a from a String to a Rc<str>, which you can clone with minimal runtime cost in order to put the value both in the K and into V. As Rc<str> is Borrow<str>, you can still look up values in the map by means of &str. This still has the - theoretical - downside that you can break the map by getting a &mut Foo from the map and std::mem::swap the a, which makes it impossible to look up the correct value from its keys; but you'd have to do that deliberately.

Another option is to actually use a HashSet instead of a HashMap, and use a newtype for Foo which behaves like a Foo.a. You'd have to implement PartialEq, Eq, Hash (and Borrow<str> for good measure) like this:

use std::collections::HashSet;

#[derive(Debug)]
struct Foo {
    a: String,
    b: String,
}

/// A newtype for `Foo` which behaves like a `str`
#[derive(Debug)]
struct FooEntry(Foo);

/// `FooEntry` compares to other `FooEntry` only via `.a`
impl PartialEq<FooEntry> for FooEntry {
    fn eq(&self, other: &FooEntry) -> bool {
        self.0.a == other.0.a
    }
}

impl Eq for FooEntry {}

/// It also hashes the same way as a `Foo.a`
impl std::hash::Hash for FooEntry {
    fn hash<H>(&self, hasher: &mut H)
    where
        H: std::hash::Hasher,
    {
        self.0.a.hash(hasher);
    }
}

/// Due to the above, we can implement `Borrow`, so now we can look up
/// a `FooEntry` in the Set using &str
impl std::borrow::Borrow<str> for FooEntry {
    fn borrow(&self) -> &str {
        &self.0.a
    }
}

fn main() {
    let foo_1 = Foo {
        a: "foo".to_string(),
        b: "bar".to_string(),
    };

    let foo_2 = Foo {
        a: "foobar".to_string(),
        b: "barfoo".to_string(),
    };

    let foo_vec = vec![foo_1, foo_2];

    let mut foo_hashmap = HashSet::new();

    foo_vec.into_iter().for_each(|foo| {
        foo_hashmap.insert(FooEntry(foo));
    });

    // Look up `Foo` using &str as keys...
    println!("{:?}", foo_hashmap.get("foo").unwrap().0);
    println!("{:?}", foo_hashmap.get("foobar").unwrap().0);
}

Notice that HashSet provides no way to get a &mut FooEntry due to the reasons described above. You'd have to use RefCell (and read what the docs of HashSet have to say about this).

The third option is to simply clone() the foo.a as you described. Given the above, this is probably the most simple solution. If using an Rc<str> doesn't bother you for other reasons, this would be my choice.

Sidenote: If you don't need to modify a and/or b, a Box<str> instead of String is smaller by one machine word.

user2722968
  • 13,636
  • 2
  • 46
  • 67