Why does using hashbrown's HashSet cause a lifetime issue but std's HashSet does not

I'm struggling to understand why the following code:

use hashbrown::HashSet;
fn foo<'a>(_: &'a HashSet<String>, _: &mut HashSet<&'a str>) {}
fn bar() -> HashSet<String> {
    let a = HashSet::new();
    let mut b = HashSet::new();
    foo(&a, &mut b);
    a
}

returns the following compilation error:

error[E0505]: cannot move out of `a` because it is borrowed
 --> src/lib.rs:7:5
  |
4 |     let a = HashSet::new();
  |         - binding `a` declared here
5 |     let mut b = HashSet::new();
6 |     foo(&a, &mut b);
  |         -- borrow of `a` occurs here
7 |     a
  |     ^ move out of `a` occurs here
8 | }
  | - borrow might be used here, when `b` is dropped and runs the destructor for type `hashbrown::HashSet<&str>`
  |
help: consider cloning the value if the performance cost is acceptable
  |
6 |     foo(&a.clone(), &mut b);
  |           ++++++++

For more information about this error, try `rustc --explain E0505`.
error: could not compile `remote` (lib) due to 1 previous error

yet the following all compile:

Replace the exclusive HashSet from hashbrown with the HashSet from std:

use hashbrown::HashSet;
use std::collections::HashSet as StdHashSet;
fn foo<'a>(_: &'a HashSet<String>, _: &mut StdHashSet<&'a str>) {}
fn bar() -> HashSet<String> {
    let a = HashSet::new();
    let mut b = StdHashSet::new();
    foo(&a, &mut b);
    a
}

Drop b via core::mem::drop:

use hashbrown::HashSet;
fn foo<'a>(_: &'a HashSet<String>, _: &mut HashSet<&'a str>) {}
fn bar() -> HashSet<String> {
    let a = HashSet::new();
    let mut b = HashSet::new();
    foo(&a, &mut b);
    drop(b);
    a
}

Drop b via local scope:

use hashbrown::HashSet;
fn foo<'a>(_: &'a HashSet<String>, _: &mut HashSet<&'a str>) {}
fn bar() -> HashSet<String> {
    let a = HashSet::new();
    {
        let mut b = HashSet::new();
        foo(&a, &mut b);
    }
    a
}

Make the exclusive HashSet in foo shared:

use hashbrown::HashSet;
fn foo<'a>(_: &'a HashSet<String>, _: &HashSet<&'a str>) {}
fn bar() -> HashSet<String> {
    let a = HashSet::new();
    let mut b = HashSet::new();
    foo(&a, &mut b);
    a
}
1 Like

All the collections in std get the privilege of having magical drops on stable, which is only available on unstable outside of std.

Namely, they use the may_dangle feature which promises that a given Drop implementation does not, itself, "look at" any borrows related to a given generic, beyond moving or recursively dropping them. This allows borrow checking to not consider drops to be a "use" of such generics (so long as the property holds recursively).

The trailing comments of that issue indicate that the feature may not be considered stable-worthy in its current form. None-the-less, as you have discovered, the feature is quite load-bearing.

7 Likes

I had an inkling that somehow std was treated special especially weird seeing how hashbrown is used by std. I suppose that's another reason to prefer std's HashMap/HashSet over hashbrown. Normally I use std, but I had a case where I needed the functionality of hashbrown. Once I use hashbrown, I typically stay consistent throughout and use hashbrown's types exclusively. I may have to re-think that philosophy and still use std even when needing hashbrown for a subset of my code.

this line of the diagnostic message is a good hint of where to look for the difference between the standard library and hashbrown, namely the destructor.

2 Likes

Yeah, I saw that; but HashSet doesn't implement Drop in std or hashbrown so I just gave up investigating further. Also the fact that dropping hashbrown's HashSet "fixed" the issue was another sign that something "fishy" was happening with how things were getting dropped. Looking further into the actual source code I see RawTable's Drop impl and also noticed that enabling the nightly feature in hashbrown causes it to use may_dangle.

the standard library uses the rustc-dep-of-std feature of the hashbrown crate, which is a feature that is "reserved" for the standard library, and nightly is transitively enabled there.

in user code, you should not use rustc-dep-of-std, but you can of course use the nightly feature if you are on nightly toolchain.

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.