Does an explicitly dropped value inhibit the implicit drop operation inserted by the compiler?

Consider this example

struct Dump(Rc<i32>);
impl Drop for Dump{
    fn drop(&mut self) {
        println!("destroy");
    }
}
fn main(){
    let d = Dump(Rc::new(1));
} // { let mut d = d; d.drop()}

As exposed by the pseudo-code in the comment, the compiler would insert such a code to destroy the variable d at the end of the scope. The behavior is that destroy is printed only once. If we explicitly drop d, such as

fn main(){
    let d = Dump(Rc::new(1));
    drop(d);
    println!("main");
}

The destroy is printed once prior to main, this means the compiler does not invoke the destructor at the end of the scope. With this behavior, I suppose that the explicit drop(d) can suppress the compiler from inserting the destroy operation for d at the end of the scope. However, consider this example

use std::future::Future;
fn spawn<F:Future>(f:F) where F:Send + 'static{}
fn show(){
   spawn(async move{
	  let d = Dump(Rc::new(1));
	  drop(d);
	  async {}.await;
   });
}

The compiler says

future is not Send as this value is used across an await

^^^^^^ await occurs here, with d maybe used later

The variable d has been dropped and moved, how could it be used after the await point? The error seems to arise from that the compiler still inserts the drop operation at the end of the scope, which results in the use of d after the await point.

So, I wonder, does the explicit drop operation suppress the implicit drop operation(that uses the variable) inserted by the compiler? Otherwise, how does the compiler solve the double-free?

Yes, if you move a value out of a variable, then the implicit drop is inhibited. The drop method moves the value out of the variable.

As for the example with .await, that's a problem in how futures are analyzed. The compiler is too strict here, and it would not be incorrect for the compiler to accept the code.

6 Likes

A possible workaround is to use a block to perform the drop:

 fn show() {
     spawn(async move {
-        let d = Dump(Rc::new(1));
-        drop(d);
+        {
+            let d = Dump(Rc::new(1));
+        }
         async {}.await;
     });
 }

(Playground)

The red and green code is equivalent, but the compiler (currently) will only be satisfied if you use the block to drop the variable. Also see issue #104883 in Rust's bug tracker.

7 Likes

Is there any document about this part?

Technically the drop function isn't special. It's just a function that takes an argument by value and does nothing with it. The compiler just inserts the drop related code that would have gone at the end of the variable's scope into the drop function instead.

Yes, I know that drop function works based on the ownership guaranteed by Rust. More generally speaking, The question is: does the move operation(i.e the variable will lose the ownership) inhibit the compiler from inserting implicitly drop operation that would have been inserted at the end of the scope in which the variable is defined?

let other = x; // x is moved
other_2 = x2; // x2 is moved

All these move operations will make the variables lose their ownership, is the compiler inhibited to insert the drop operation for x, and x2 at the end of the scope?

I'm not entirely sure I understand what you're trying to ask. One of the primary goals of the ownership system is to ensure safe code can't produce "double drops" where one value gets dropped twice.

A variable which has been moved out of cannot be dropped. If the compiler can't know statically whether a variable has been moved out of (for example because it may be moved out of in a conditional block) then the compiler adds a drop flag so the generated drop code can be skipped for that variable.

3 Likes

I asked how the compiler knows whether it should insert the implicit drop operation for a moved variable. Then you give the answers that the compiler uses the drop flag, which is what I want to know.

So, the last issue is, in the latter case(the async function), the compiler already knows the variable d is dropped before the await point, i.e., there is no further drop operation to be inserted after the await operation, why does the compiler say we use the d after the await point?

Hmm I think what's happening there is that the variable is still considered to exist in that scope, even though it's been moved out of. If you use an extra block to scope the variable, it works

Playground

fn show() {
    spawn(async move {
        {
            let d = Dump(Rc::new(1));
            drop(d);
        }

        async {}.await;
    });
}

Looks to be issue #57478

Documentation

Reference: Destructors

When an initialized variable or temporary goes out of scope, its destructor is run, or it is dropped. Assignment also runs the destructor of its left-hand operand, if it's initialized. If a variable has been partially initialized, only its initialized fields are dropped.

Reference: Glossary: Initialized

A variable is initialized if it has been assigned a value and hasn't since been moved from.

Nomicon: Drop Flags


@alice and @jbe already addressed this -- the compiler's analysis in that context isn't yet good enough.

3 Likes

Incidentally, here's another issue with some interesting notes on drop elaboration. It's about drops in const but the async issues are related -- more drop information is needed to improve things, but currently that information comes from a later pass, and getting the approximation wrong results in miscompilations (sometimes even unsound ones).

If you're interested in more technical implementation details, there's a section in the dev guide.

2 Likes

I believe this might help: Running Code on Cleanup with the Drop Trait - The Rust Programming Language

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.