Lots of references when using HashMap

I have this simple HashMap example:

use std::collections::HashMap;

fn main() {
    let mut my_map = HashMap::<u32, f64>::new();
    my_map.insert(2, 2.71);
    my_map.insert(3, 3.41);

    if my_map.get(&2).unwrap_or(&0.0) > &3.0 {
        println!("Value is greater than 3");
    } else {
        println!("Value is less than 3");
    }
}

Questions:

  1. The insert function takes inputs k: K, v: V, which are values. This makes sense. However, when I try to get a value from the map, why is it that the key parameter &2 has to be a reference now?
  2. Since get returns Option<&V>, I (kind of) understand why the default &0.0 has to be a reference too. However, similar to my other question (Why can we not compare Values & References?), why wouldn't the > operator work with 3.0? Why do we have to write &3.0?
  3. What are the lifetimes of these three "temporary" references: &2 and &0.0 and &3.0? Is this Rust code entirely correct and as intended? I just ask, because I would feel much more comfortable if they were just simple values (because then, the question of lifetime wouldn't even come up).

Any insights much appreciated! :slight_smile:

Because if it were something expensive to construct, it would be a waste to have to pass it by value just to do a lookup, since the lookup doesn't need to consume it. For an i32 that's not important, but that's why the API is that way.

And note that it's not directly a reference to the key type; it uses Borrow: https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.get. That way you can lookup by &str into a HashMap<String, _>.

Yes, this is fine. In safe code, you're good with lifetimes if it compiles. (It might be over-constrained, but it won't be wrong.)

Lifetimes of temporaries are a complicated question. The following desugaring isn't quite right, but it's vaguely in the right direction to hopefully help:

match (2, 0.0, 3.0) {
    (temp1, temp2, temp3) =>
        if my_map.get(&temp1).unwrap_or(&temp2) > &temp3 {
            println!("Value is greater than 3");
        } else {
            println!("Value is less than 3");
        }
}

(Well, that's how it works for more complicated temporary types. This version might be doing rvalue static promotion.)

The key to the design is to avoid making clones whenever possible. This is because cloning could be very expensive, so the designers of the library don't want to force you to clone needlessly, either keys or values.

So most methods take references and if relevant return references. The references you pass are always short-lived, just the duration of the function call. Those that are returned to you, you can hang onto as long as you like, but they're borrowed from the map, so you can't modify it in the meantime. You can easily clone the value to drop the reference.

Thanks for insightful answers. Especially the detail about Borrow was new (and interesting) to me.


So just to double-check. The idiomatic and only correct way is to write

if my_map.get(&2).unwrap_or(&0.0) > &3.0 { ... }

with all three & ?

For example, I ask, because couldn't unwrap_or(0.0) automatically treat 0.0 as a const reference (i.e. &0.0) by inserting & in the background? Couldn't one argue that & is unambiguously implied? Or maybe it is not?

Similar to the C++ equivalent where value 2 goes from int (at the time of being passed into the function) to const int& (when inside the function):

void get(const int& key) {
    // ...
}

int main() {
    get(2);
}

Apologies for asking such a naive question. I am just trying to understand Rust so I can use it the way it is intended.

That would be the "discarding ownership" idea, see https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html#ideas-implied-borrows.

It would be possible, and I personally like it, but I know that others have concerns about it too. There was hope to look at this and other related ideas (Tracking issue for experiments around coercions, generics, and Copy type ergonomics · Issue #44619 · rust-lang/rust · GitHub), but that never really ended up happening.

Note that Rust references are very different from C++ references, because C++ references try to hide from you (see why reference_wrapper needs to exist). You can easily have a &&&&&[i32] in Rust -- you wouldn't ever write that out explicitly as a function parameter type, but it's possible.

This shows up in other places too. In C++ you can't re-point a reference to point at a different lvalue, because assignment applies to the pointee, not the reference itself. Whereas in Rust you can have let mut r: &i32; and change the reference to point at different places even though you can't change the values inside those places.

I would say what you did there is idiomatic.

There are some other correct possibilities, like

if *my_map.get(&2).unwrap_or(&0.0) > 3.0 { ... }

or

if my_map.get(&2).copied().unwrap_or(0.0) > 3.0 { ... }

but it's not obvious to me that those are better.

And, importantly, once you're using something more complex than a trivial Copy type, the references start to matter more.

For simple Copy numbers, I'd prefer the dereferenced version, which makes it a little more friendly to include arithmetic and variables. But for larger or non-Copy types, we definitely want to use references, as you say.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.