Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do I need a double ampersand when getting values from a HashMap?

Tags:

rust

I'm having some trouble with references in rust. I have the following code that does not compile:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();

    map.insert(&0, &0);
    map.insert(&1, &1);

    assert_eq!(map.get(&0), Some(&0));
}

The compilation error I get is:

error[E0308]: mismatched types
 --> rust_doubt.rs:9:5
  |
9 |     assert_eq!(map.get(&0), Some(&0));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected &{integer}, found integral variable
  |
  = note: expected type `std::option::Option<&&{integer}>`
             found type `std::option::Option<&{integer}>`
  = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

error: aborting due to previous error

Sure enough, if I change the line:

assert_eq!(map.get(&0), Some(&0)); to assert_eq!(map.get(&0), Some(&&0)); (double ampersand) the code compiles

Questions:

  1. map.insert(&0, &0) inserts pointers to two integer literals into the map. I'm not sure how this is even possible since I have not used a variable anywhere. How can I have a reference to a literal? I was expecting the compiler to make me do this:
let a = 0;
let b = 0
map.insert(&a, &b);

In other words, what does &0 even mean? Does it allocate memory for the literal and return a reference to it? If so, then am I correct in assuming that no two &0s would point to the same memory?

  1. Why do I have to do Some(&&0) instead of just Some(&0)? What does &&0 even mean? I understand that **ptr means dereferencing a variable twice to get the underlying value. But I can't quite imagine the reverse - how can you "refer" an integer literal twice?
like image 782
Kevin Martin Jose Avatar asked Mar 05 '23 10:03

Kevin Martin Jose


1 Answers

If you look at the signature of insert and get you will realize that they handle things differently.

Starting from a HashMap<K, V>:

  • fn insert(&mut self, k: K, v: V) -> Option<V>.
  • fn get(&self, k: &K) -> Option<&V> (simplified).

As you can see, insert takes ownership, handling values, while get takes and returns a reference.

Therefore, if you insert &1, you get Some(&&1) back: one more layer of reference.


The question, then, is why is there no error from .get(&0): isn't it lacking a level of reference?

Well, I cheated and simplified the signature of get, the exact signature is:

pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> where
    K: Borrow<Q>,
    Q: Hash + Eq, 

And it turns out that &T implements Borrow<T>, so you can call get with &K for &&K.


If you manage to get the compiler to give you the type of the HashMap, it's a bit easier:

assert_eq!(map, ());

Results in:

error[E0308]: mismatched types
 --> src/main.rs:9:5
  |
9 |     assert_eq!(map, ());
  |     ^^^^^^^^^^^^^^^^^^^^ expected struct `std::collections::HashMap`, found ()
  |
  = note: expected type `std::collections::HashMap<&{integer}, &{integer}>`
             found type `()`
  = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

Which shows you what type the compiler figured out for K and V, and indeed it will be &{integer}, since you pass &0 to insert which takes key and value by value.


As for the issue of lifetimes:

  1. Not all checks are done in a single pass. In particular, borrowing/lifetime checks are generally done after type checking.
  2. Literals have the 'static lifetime, just like "Hello" has the &'static str type.

The compiler automatically reserves memory somewhere in the program for literals, and will "borrow" them as necessary. This means that creating a reference to a literal integer is perfectly fine: &0i32 has type &'static i32.

like image 82
Matthieu M. Avatar answered Apr 19 '23 23:04

Matthieu M.