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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions