Rust uses RAII (Resource Acquisition Is Initialization) to manage resources: object initialization results in the initialization of a resource, and object release results in the release of a resource.

Take Mutex as an example.

1
2
3
4
5
6
7
8
9
{
    let guard = m.lock();
    // do something
}
// guard freed out of scope.
{
    // we can acquire this lock again.
    let guard = m.lock();
}

When guard leaves the current scope, rust ensures that drop of guard is called automatically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
    #[inline]
    fn drop(&mut self) {
        unsafe {
            self.lock.poison.done(&self.poison);
            self.lock.inner.raw_unlock();
        }
    }
}
  • If the corresponding type has its own Drop implementation, rust calls Drop::drop().
  • Otherwise, the automatically generated drop implementation is executed recursively for each field.

Drop’s trait is defined as follows.

1
2
3
pub trait Drop {
    fn drop(&mut self);
}

It’s very simple, but it’s still easy to encounter pitfalls in the actual use process.

Differences in behavior between _ and _var

The semantics of let _var = abc; is very clear: a new binding is created, and its lifetime lasts until the end of the current scope.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    {
        println!("into scope");
        let _abc = Test("_abc");
        println!("leave scope");
    }
}

The result of its execution is as follows.

1
2
3
into scope
leave scope
Test with _abc dropped

But the semantics of let _ = abc; is a bit more obscure: don’t bind the subsequent expression to anything. It’s just a match expression that doesn’t cause a drop per se; the reason we observe a drop is that the value it matches is itself temporary.

Many people interpret this as equivalent to drop(abc) or abc; is wrong, here is a counterexample.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    let x = Test("x");
    {
        println!("into scope");
        let _ = x;
        println!("leave scope");
    }
}

The result of its execution is as follows.

1
2
3
into scope
leave scope
Test with x dropped

Interpreting this as a no-op is also one-sided. We can likewise identify a counterexample.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    println!("into scope");
    let _ = Test("x");
    println!("leave scope");
}

The result of its execution is as follows.

1
2
3
into scope
Test with x dropped
leave scope

Here are some more interesting examples.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    match (Test("a"), Test("b"), Test("c")) {
        (a, _, c) => {
            println!("match arm");
        }
    }
    println!("after match");
}

The result of its execution is as follows.

1
2
3
4
5
match arm
Test with c dropped
Test with a dropped
Test with b dropped
after match

The lifecycle of Test("b") continues until the end of this match statement.

In summary, let _ = abc; is a continuation of the match pattern, and its actual behavior is governed by the specific expression. We should not rely on let _ = abc; to implement any drop logic, and its only reasonable use is to mark variables as no longer in use, to dispense with the #[must_use] warning.

1
2
// remove file, but don't care about its result.
let _ = fs::remove_file("a.txt");

In the actual business logic, we often overlook this, taking a bug that Databend recently fixed: Bug: runtime spawn_batch does not release permit correctly. Databend uses a semaphore to control the parallelism of tasks in order to control the number of concurrent IOs. It is expected to release the semaphore after the task is executed, but the code uses let _ = permit, which causes the permit to be released at a time that is not expected, and thus the concurrency of the task is not controlled as expected.

1
2
3
4
5
6
 let handler = self.handle.spawn(async move {
     // take the ownership of the permit, (implicitly) drop it when task is done
-    let _ = permit;
+    let _pin = permit;
     fut.await
 });

How to call drop manually

For obvious reasons, Drop::drop() is not allowed to be called manually, otherwise it is very prone to double free problems, and Rust’s compiler will report an error for such a call. If you want to control the drop of a variable, you can use the std::mem::drop function, which is very simple: Move the variable and return nothing.

1
2
3
4
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "mem_drop")]
pub fn drop<T>(_x: T) {}

Essentially equivalent to the following.

1
2
3
4
5
let x = Test {};

{
    x;
}

Note, however, that for types that implement Copy, it makes no sense to call drop.

  • The compiler maintains the Copy type’s data on the stack itself, and cannot implement the Drop trait for the Copy type.
  • Calling drop on a Copy type always copies the current variable and releases

Ref