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.
What the compiler says
Section titled “What the compiler says”use std::collections::HashMap;
fn main() { let mut scores: HashMap<f64, &str> = HashMap::new(); scores.insert(1.5, "one and a half"); println!("{}", scores.len());}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.
Why NaN rules out Eq
Section titled “Why NaN rules out Eq”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:
fn main() { let nan = f64::NAN; println!("{}", nan == nan); println!("{}", nan != nan); println!("{}", nan.is_nan());}falsetruetrueA 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.
Key on the bits
Section titled “Key on the bits”to_bits() is the fix that needs no new types: it returns the u64 bit pattern, and integers
implement both traits.
use std::collections::HashMap;
fn main() { let mut scores: HashMap<u64, &str> = HashMap::new(); scores.insert(1.5_f64.to_bits(), "one and a half"); println!("{:?}", scores.get(&1.5_f64.to_bits()));
scores.insert(0.0_f64.to_bits(), "zero"); scores.insert((-0.0_f64).to_bits(), "minus zero"); println!("{}", scores.len());}Some("one and a half")3The 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.
When every NaN should be one key
Section titled “When every NaN should be one key”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:
use std::collections::HashMap;use std::hash::{Hash, Hasher};
struct FloatKey(f64);
impl PartialEq for FloatKey { fn eq(&self, other: &Self) -> bool { self.0 == other.0 || (self.0.is_nan() && other.0.is_nan()) }}
impl Eq for FloatKey {}
impl Hash for FloatKey { fn hash<H: Hasher>(&self, state: &mut H) { if self.0.is_nan() { f64::NAN.to_bits().hash(state); } else if self.0 == 0.0 { 0_u64.hash(state); } else { self.0.to_bits().hash(state); } }}
fn main() { let mut scores: HashMap<FloatKey, &str> = HashMap::new(); scores.insert(FloatKey(1.5), "one and a half"); scores.insert(FloatKey(f64::NAN), "not a number"); println!("{}", scores.len()); println!("{:?}", scores.get(&FloatKey(-f64::NAN)));}2Some("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.
Related
Section titled “Related”- Which collection should you use? — where the
Hash+Eqrequirement on keys comes from - How to sort a Vec — floats have no
Ordeither, same root cause - How to deduplicate a Vec — NaN survives
dedup, the same contract
Next in this chapter: how to handle missing keys with the entry API.