Skip to content
Select theme

Why can't I use f64 as a HashMap key?

Short answer: f64 implements PartialEq but neither Eq nor Hash, and HashMap keys need both. Eq is the one with a documented reason: NaN compares unequal to itself, which breaks the reflexivity the trait promises. Key on something that has both traits instead — the integer bits from to_bits(), or a wrapper type that defines its own equality.

From a fresh project (cargo new f64-key-demo), replace the contents of src/main.rs with each block below and run it with cargo run.

error[E0599]: the method `insert` exists for struct `HashMap<f64, &str>`, but its trait bounds were not satisfied
--> src/main.rs:5:12
|
5 | scores.insert(1.5, "one and a half");
| ^^^^^^
|
= note: the following trait bounds were not satisfied:
`f64: Eq`
`f64: Hash`
For more information about this error, try `rustc --explain E0599`.

The note names both traits, and HashSet refuses f64 for the same pair. The map finds an entry by hashing the query into a bucket, then comparing for equality inside it; the standard library’s rule is k1 == k2 implying hash(k1) == hash(k2), and NaN defeats the comparison step outright.

The standard library says so on the trait itself: floating-point types “implement only PartialEq but not Eq because NaN != NaN”.

Eq is PartialEq plus one more promise. PartialEq already guarantees the comparison is symmetric and transitive; Eq adds reflexivity — a == a for every value — and since no compiler can check that, it is a trait without methods. NaN fails it:

false
true
true

A NaN is equal to nothing, including itself. Insert one as a key and it can never be found: the lookup compares query and stored key, and that comparison is false. The compiler refuses the map rather than hand you keys that vanish. Floats have no Ord either, same root cause; the sorting page works through that one.

to_bits() is the fix that needs no new types: it returns the u64 bit pattern, and integers implement both traits.

Some("one and a half")
3

The standard library calls to_bits a “raw transmutation to u64”, not a cast. The catch: bits are the representation, not the value — 0.0 and -0.0 compare equal but have different bit patterns, so they enter the map as two keys, which is where the 3 comes from. Two NaNs with different payloads are two keys as well. When the input never mixes +0.0 with -0.0, and distinct NaN keys are harmless, this is the cheapest fix.

Wrap the float and implement the two traits by hand. Eq has no methods, so its impl is empty; the work is keeping the two in agreement:

2
Some("not a number")

Every NaN is now one key: -f64::NAN, a different bit pattern, finds what f64::NAN stored. The Hash impl has to follow PartialEq wherever equality groups values the plain comparison doesn’t — all NaNs hash to one constant, and +0.0 and -0.0 share the zero hash because they compare equal. Miss one of those pairs and the key lands in a bucket the lookup never visits.

A third option avoids the problem: if a NaN has no business being a key, keep it out with if value.is_nan() { continue; } before the insert. The ordered_float crate ships both wrappers (OrderedFloat and NotNan) for readers who would rather not maintain them.

Next in this chapter: how to handle missing keys with the entry API.