4

I have a struct Foo<'a> which is a wrapper around &'a str references. And I want to populate a HashMap with Foos as keys. Here is a snippet of code (open it in playground):

use std::collections::HashMap;

#[derive(PartialEq, Eq, Hash)]
struct Foo<'a> {
    txt: &'a str,
}

fn main() {
    let a = "hello".to_string();
    let a2 = Foo { txt: &a };
    let b = "hello".to_string();
    let b2 = Foo { txt: &b };

    let mut hm = HashMap::<Foo, u32>::new();

    hm.insert(a2, 42);
    println!("=== {:?}", hm.get(&b2));     // prints Some(42)
    println!("=== {:?}", hm.get_mut(&b2)); // prints Some(42)

    {
        let c = "hello".to_string();
        let c2 = Foo { txt: &c };
        println!("=== {:?}", hm.get(&c2));         // prints Some(42)
        // println!("=== {:?}", hm.get_mut(&c2));  // does not compile. Why?
        // hm.insert(c2, 101);                     // does not compile, but I understand why.
    }
}

This code compiles and runs perfectly, but the compiler complains if I uncomment the two last lines of code. More precisely, it complains about the borrowed value in c2 not living long enough.

For the last one (insert), this is perfectly understandable: I can not move c2 into the HashMap, which lives longer than data borrowed by c2 from c.

However, I don't understand why the second-to-last line (get_mut) has the same problem: in that case, the borrowed data should only be necessary during the call to get_mut, it is not moved into the HashMap.

This is all the more surprising that the get above works perfectly (as I expected), and that both get and get_mut have identical signatures when it comes to the k parameter...


After digging a little more, I reproduced the problem with plain references (instead of a struct embedding a reference).

use std::collections::HashMap;

fn main() {
    let a = 42;
    let b = 42;

    let mut hm = HashMap::<&u32,u32>::new();

    hm.insert(&a, 13);
    println!("=== {:?}", hm.get(&&b));     // prints Some(13)
    println!("=== {:?}", hm.get_mut(&&b)); // prints Some(13)

    {
        let c = 42;
        println!("=== {:?}", hm.get(&&c));        // prints Some(13)
        //println!("=== {:?}", hm.get_mut(&&c));  // does not compile. Why?
    } 
}

(open in playground)

Again, uncommenting the last line causes the compiler to complain (same message as above).

However, I found an interesting workaround for this particular example: replacing &&c by &c in the last line solves the problem -- actually, one can replace && by & in all calls to get and get_mut. I guess this has to do with &T implementing Borrow<T>.

I don't understand precisely what, in this workaround, convinces the compiler to do what I want it to do. And I can not apply it directly to my original code, because I don't use references as keys, but objects embedding references, so I can not replace && by &...

Pierre-Antoine
  • 1,915
  • 16
  • 25
  • I believe this question to already be answered by [Why does linking lifetimes matter only with mutable references?](https://stackoverflow.com/q/32165917/155423). If you disagree, please [edit] your question to explain how this differs from the existing answers. Otherwise, we can mark this as already answered. – Shepmaster May 07 '18 at 19:51
  • Additionally, how does *the compiler know* that `get_mut` isn't going to store the argument in the `HashMap`, using only the signature of the function? – Shepmaster May 07 '18 at 19:54
  • I'm not entirely sure that my question is the same as the one you referred to. Mostly, the difference is that in my case, only immutable references to `Foo` (the borrowing type) are involved. – Pierre-Antoine May 07 '18 at 20:02
  • However, you are spot on regarding `get_mut`: the difference (with `get`) is not about parameter `k`, but about `self` being mutable. – Pierre-Antoine May 07 '18 at 20:21
  • Right, it's the difference between `&self` and `&mut self`. Thanks to lifetime elision, the method is `get_mut<'a, Q: ?Sized>(&'a mut self, k: &Q) -> Option<&'a mut V>` — the lifetimes are linked. – Shepmaster May 07 '18 at 20:27
  • Yes, but (my understanding of) lifetime elision does not explain everything here. To be even more precise, we have `get_mut<'a, 'b, Q: ?Sized>(&'a mut self, k: &'b Q) -> Option<&'a mut V>`. But here the "inner" lifetime of Q seems to be constrained by `'a`. – Pierre-Antoine May 07 '18 at 20:44
  • 1
    If the answer to your question contains the word "variance", you're gonna have a bad time. For would-be answerers, here are some facts I found illuminating: `&T` is variant in `T`, `&mut T` is invariant in `T`, and `HashMap` is variant in `K`. Good luck! – trent May 08 '18 at 02:36

1 Answers1

7

The problem arises because an immutable reference is variant over its (referenced) type whereas a mutable reference is invariant over its type.

A great read for understanding the concept is the Nomicon.

HashMap: get versus get_mut

Shrinking further down, this is a simpler code reproducing the problem:

#![allow(unused_variables)]
use std::collections::HashMap;

fn main() {
    let mut hm = HashMap::<&u32, u32>::new();    // --+ 'a
    let c = 42;                                    // |  --+ 'b
                                                   // |    |
    HashMap::<&u32, u32>::get(&mut hm, &&c);       // |    |
    // HashMap::<&u32, u32>::get_mut(&mut hm, &&c);// |    |
}                                                  // +    +

The immutable case

Consider the signature of HashMap::get:

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

In this case &Q is &&'b u32 and get's receiver is &Self.

The variant nature of immutable references implies that a &HashMap<&'a u32, u32> can be used where a &HashMap<'b u32, u32> is required.

Thanks to this rule, the compiler considers the original invocation:

HashMap::<&'a u32, u32>::get(&hm, &&'b c);

equivalent to:

HashMap::<&'b u32, u32>::get(&hm, &&'b c);

The compiler infers from the interface, and only from the interface, that the method implementation can not introduce leaks: compilation succeeds.

The mutable case

Consider the signature of HashMap::get_mut:

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

Also in this case &Q is &&'b u32, but get_mut's receiver is &mut Self.

The invariant nature of mutable references implies that a &mut HashMap<&'a u32, u32> can not be used where a &mut HashMap<&'b u32, u32> is expected.

Thanks to this rule the compiler throws an error because by analyzing only the interface:

HashMap::<&'a 32, u32>::get_mut(&mut hm, &&'b c);

the compiler can not exclude, for example, that get_mut might store a key with lifetime 'b.

Such a key can not outlive the hm HashMap: compilation fails.

trent
  • 25,033
  • 7
  • 51
  • 90
attdona
  • 17,196
  • 7
  • 49
  • 60
  • Thanks a lot, that clarifies a lot of things (also because I read [this](https://stackoverflow.com/questions/32165917/why-does-linking-lifetimes-matter-only-with-mutable-references) before, on @shepmaster's advice). That being said, this is a pity here, because we know (unlike the compiler) that `get_mut` will not leak the passed key in the `HashMap` :-/ – Pierre-Antoine May 08 '18 at 10:46
  • Now it is right. Many thanks @trentcl for the correction! – attdona May 08 '18 at 13:29
  • I still don't get why replacing `&&c` by `&c` works, though... I understand why it works type-wise (`K=&u32` implements `Borrow` where `Q=u32`); but I don't understand why, all of a sudden, the compiler is satisfied with the lifetimes in that case... – Pierre-Antoine May 08 '18 at 14:45
  • [`&'a u32` implements `Borrow` for any lifetime `'a`](https://doc.rust-lang.org/src/core/borrow.rs.html#95-97), and [`&'a u32` implements `Borrow<&'a u32>` by the reflexive implementation](https://doc.rust-lang.org/src/core/borrow.rs.html#85-87), but `&'a u32` *doesn't* implement `Borrow<&'b u32>` even if `'a: 'b`. This is because traits, `Borrow` included, are always invariant over their parameters. – trent May 08 '18 at 18:13
  • 1
    @Pierre-Antoine This also means that if you're willing to create a newtype struct to put in your `HashMap`, you can write that missing blanket `impl` yourself and get (almost) the original example to compile: [playground](https://play.rust-lang.org/?gist=28abb8003a1e2f9460f01468b9feeb5a&version=stable&mode=debug). So your intuition is right: the restriction is overly conservative. – trent May 08 '18 at 18:18
  • @trentcl Darn, I had the idea of the FooKey, but for some reason could not get it working. I'll start afain from your code, thanks. Note that FooKey must also implement Hash and Eq, consistently with Foo, in order to respect the contract of HashMap... – Pierre-Antoine May 08 '18 at 20:29
  • @Pierre-Antoine If you pass `&c` it works because passing *values by reference* is safe and `&&c` does not work because passing *references by reference* is not safe. See [this](https://gist.github.com/attdona/52126d892a38f4c8d2593639c7579905) just to get the idea. – attdona May 08 '18 at 20:33