Skip to content

Undocumented assumption: HashMap breaks if Key::clone() does not preserve Hash and Eq #629

@gewitternacht

Description

@gewitternacht

If the Clone implementation of a HashMap's key produces a key with a different hash, or if key.clone() == key does not hold, then this can result in a broken HashMap after map.clone() (same for HashSet).

For example, consider this struct with a custom Clone implementation:

#[derive(PartialEq, Eq, Hash, Debug)]
struct FakeInt(u64);

impl Clone for FakeInt {
    fn clone(&self) -> Self {
        FakeInt { 0: 0 }
    }
}

fn test_fake_int() {
    let mut map = HashMap::new();
    map.insert(FakeInt { 0: 1 }, 1);
    map.insert(FakeInt { 0: 2 }, 2);
    let c = map.clone();
    println!("{:?}", c); // gives {FakeInt(0): 1, FakeInt(0): 2}
    assert!(c.get(&FakeInt { 0: 0 }).is_none());
}

After cloning, FakeInt(0) is in the map twice in a way, but at the same time cannot be queried or removed. This playground link contains the above example and an additional one, where Hash is preserved, but x.clone() == x does not hold, which still leads to weird behaviour due to duplicate keys.

The underlying cause seems to be that HashMap::clone() relies on Key::clone() to preserve the key's Hash, placing the cloned value in the same bucket as the original one, and also assumes that key.clone() == key, not checking for duplicate keys after cloning. However, these assumptions don't seem to be documented anywhere: Clone does not require x.clone() == x, and I couldn't find any notes on this in the Hash, Eq, HashMap or HashSet documentation, or any discussions or issues on this topic at all.

Being fairly new to Rust, I don't know whether HashMap and HashSet should be able to handle such weird Clone implementations, but I would at least expect these assumptions to be made explicit somewhere – perhaps as another "logic error" for HashMap and HashSet?

(How I came across this issue: I wanted to write a specification for HashSet::clone() with Verus, and for this tried to figure out which guarantees HashSet::clone() can provide. Digging into the implementation, I realized that RawTable::clone_from_impl() seems to silently assume that cloning a key preserves its hash and produces a key equal to the original one.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions