Structures that do not implement Drop trait

I am using some variables of type std::collections::HashMap in my code. Is the memory allocated to them freed up when their scope ends? I am asking this because it does not implement Drop trait (link referred : HashMap in std::collections - Rust).

Also,I tried to use the statement drop(hashmap) which does not give any error. I am not sure why. According to the description in the link, drop in std::mem - Rust, drop function "Disposes of a value. This does so by calling the argument’s implementation of Drop".

My aim is to ensure that the hashmap value is dropped when its scope ends.

Of course. When the structure goes out of scope, each of its fields is dropped, even if the struct itself does not have an explicit Drop implementation; and for HashMap, it contains (indirectly) the hashbrown::RawTable, which has an explicit Drop implementation.

Because drop function doesn't have any bounds on its argument (apart from Sized) and so can be called on any owned value.

This is just a description of behavior, not a specification. In fact, the only thing drop does is explicitly moving value out of scope. After that, it will be dropped using the common rules.

4 Likes

Thanks! This cleared things up for me.

Note however, that this:

...is not possible in general, because:

  • map can be explicitly forgotten;
  • map can be wrapped in ManuallyDrop, which is special and does not drop the inner value when going out of scope;
  • map can be a part of reference cycles, i.e. inside an Rc or Arc which somehow contains a clone of itself.

But if you have a bare HashMap and don't forget it yourself, it will always be dropped.

3 Likes

Not using forget, ManuallyDrop or Rc or Arc anywhere. The code structure using hashmap is something like this :

struct A{
    id : usize,
}
async fn function() {
    //create_hashmap() initialises the hasher, inserts key-value pairs and returns a hashmap
    let hashmap: HashMap<String,A,fasthash::RandomState<xx::Hash64>> = create_hashmap();
    ...
    function1(&hashmap).await;
    ...
}

async fn function1(hashmap : &HashMap<String,A,fasthash::RandomState<xx::Hash64>>){
    ...
    let x = function2(hashmap).await;
    ...
}

async fn function2(hashmap : &HashMap<String,A,fasthash::RandomState<xx::Hash64>>){
    ...
    let k = "h";
    //accessing hashmap entries
    let entry = (*hashmap).get(&k);
    ...
}

This is a place where the [src] link is useful, because drop is actually one of the simplest functions possible in Rust.

It's literally just this:

pub fn drop<T>(_x: T) {}

Nothing special going on at all.

2 Likes

I would like to add that even if the function seemingly doesn't do anything, the effect of std::mem::drop is caused by the fact that calling it with the value as an argument will transfer ownership of the value to the function (which then does nothing with the value).

Thus even if the function is empty, calling it has an effect.

struct S {
    number: i32,
}

impl Drop for S {
    // this is `std::ops::Drop::drop`
    fn drop(&mut self) {
        println!("- Dropped {}", self.number)
    }
}

fn my_drop<T>(_value: T) {
    // do nothing (same as in `std::mem::drop`)
}

fn main() {
    println!("Compare:");
    {
        let _a = S{ number: 5 };
        let _b = S{ number: 640 };
    }
    println!("With:");
    {
        let a = S{ number: 5 };
        let b = S{ number: 640 };
        my_drop(a);
        my_drop(b);
    }

}

(Playground)

Output:

Compare:
- Dropped 640
- Dropped 5
With:
- Dropped 5
- Dropped 640

Also note that std::ops::Drop::drop is not the same as std::mem::drop, even if those two are related.

  • std::ops::Drop::drop is invoked automatically and works on &mut self. It cannot be called manually but you can implement it to execute custom destructor code when a value is dropped.
  • std::mem::drop has no magic at all. It's just an "empty" function, but since it is called by-value, calling it with a value will cause the value to be dropped. You use std::mem::drop (or just drop) when you want to explicitly drop a value.

Another side note: When a type implements Copy, then drop(value_with_such_type) will have no effect (I stumbled upon this a couple of times), because the value will be copied first, before it's dropped by drop.

fn main() {
    let x = 9;
    println!("x is {x}");
    drop(x); // x can be `Copy`ed, thus `drop` doesn't make the next line invalid
    println!("x is still there: {x}");
}

(Playground)

Output:

x is 9
x is still there: 9

This isn't a problem in practice though, because types that implement Copy will have no destructors:

Copy can only be implemented for types which do not implement Drop, and whose fields are all Copy.

(from the reference)

Note that Rust tries to be the least surprising possible in general. If you don't hold onto a value, it will clear up after itself, even if it doesn't explicitly implement Drop.

This is because implementing Drop is not the same as "needing cleanup". The literal Drop impl only applies to the very specific type it is implemented for. It literally only means "run this code when I go out of scope", nothing more.

Now, Rust recognizes the transitive need for cleanup as a built-in language feature. Thus, anything that contains a Drop type but it is not itself Drop will be cleaned up, by transitively dropping the fields/variants of values of such types. This isn't done either by the concrete implementations of Drop::drop() or the (even more non-magical) mem::drop(). This is done by the compiler itself, which keeps track (at compile time) whether a type needs cleanup, and if so, it will insert the so-called "drop glue" performing transitive cleanup whenever it sees a value going out of scope.

So, the implications are:

  • Checking whether a type is Drop is not useful for determining whether it needs cleanup (in fact it is impossible eg. for generic types without knowing the concrete substitutions).
  • In general, you don't need to worry about Drop being called correctly at all (except for unsafe and the corner cases wrt. reference cycles or manually forgetting).
  • You don't need to write explicitly recursive/transitive Drop impls, either. For example, if you are writing a data structure with safe code that contains a Vec as its backing storage, then you don't need to add an impl Drop just to perform drop(self.vec).

In general, you can expect Rust to behave sanely and as expected most of the time; the language is very carefully designed to be free of common footguns. (This separates it from basically any other systems language out there.) As long as you stay in safe code and within the realm of the single-ownership-and-no-mutable-aliasing system (which you should be in like 99% of the time), you likely won't need to be concerned with such low-level memory management details at all.

The section in the Rust reference on destructors also explains that the destructor is more than what's done in std::ops::Drop::drop. Automatic invocation of Drop::drop (if a type implements Drop) is just one part of the destruction process.

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.