Force a value to be dropped at some point

This example shows that Rust remembers somewhere at runtime that the value is
already dropped (consumed) and an auto-drop at the end of scope is not needed. But
how to force the value to be dropped at some point? Why can't I use Rust's
knowledge about the state of the value? Note this is possible in C++ by moving a
value into a temporary, which is a no-op for already moved values.

struct My {}

impl Drop for My {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn main() {
    let (x, y) = (My {}, My {});
    match 1 < 2 {
        true => drop(x),
        false => drop(y),
    }
    // drop(y); // What should I do?
    println!("This code requires `y` to be dropped!");
    println!("end of main");
}
2 Likes

The usual way to make sure something is dropped is to restrict its scope.

fn main() {
    let x = My {};
    {
        let y = My {};
        match 1 < 2 {
            true => drop(x), // x will be moved here and dropped
            false => drop(y),
        }
    } // y will be dropped here automatically
    println!("This code requires `y` to be dropped!");
    println!("end of main");
} // x would be dropped here if it hadn't already been

It's relatively uncommon to call drop explicitly.

1 Like

You could also move y into the scope.

fn main() {
    let (x, y) = (My {}, My {});
    {
        let y = y;  
        [...]
    }
    [...]
}
1 Like

Looks reasonable. I think most of the times the code should be written this way to make programmer's intention more explicit. But I just wonder why can't I use Rust's runtime info about the value state. This seems restrictive with no benefit for safety.
My example is very simple. If I add more conditions or even loops with drop() inside, it may be difficult to write an equivalent just using scopes. But I found an ugly solution :slight_smile: :

fn main() {
    let (mut x, mut y) = (Option::Some(My {}), Option::Some(My {}));
    match 1 < 2 {
        true => x = None,
        false => y = None,
    }
    y = None;
    println!("This code requires `y` to be dropped!");
    println!("end of main");
}

Btw, this produces many warnings about unused vars, but they are actually used because of the side effect of calling [implicit] drop() on them.

In general Rust's info about the value state is compile-time only. Rust does not have a garbage collector or other significant runtime. Whether due to end of scope, end of use (NLL), or explicit Drop calls, any runtime drop code is inserted at compile-time.

2 Likes

I'm curious, what possible reason is there for wanting to force something to be dropped? Do you have a real world use case?

1 Like

As one example, Mutex.lock() returns a guard object that will prevent any other thread from acquiring the lock until it is dropped. If you’re done with the locked value but still need to do more computation, you might want to force the guard to drop so that other threads can access the lock while you’re working.

2 Likes

But surely if you are done with the locked value you can arrange to exit whatever scope the guard object is in and continue working on something else? At which point the guard is dropped.

Or am I missing a point here?

Exiting the relevant scope is generally the preferred method to force a drop, but sometimes it’s awkward to arrange things that way. You might need the locked value to decide which of two branches to go down but only one of them needs to continue holding the lock, for example:

{   let x = mutex.lock();
    if x.check_something() {
        x.do_some_calculations();
    } else {
        mem::drop(x);
        // do different expensive calculations
    }
}

Freeing it after the check and reacquiring in the branch risks some other thread taking the lock in between, and holding it in the branch where it’s not needed is wasteful.

1 Like

I'm sure there are ways to arrange use of x into a scope so it gets dropped as soon as it is not needed:

    let more = true;
    {
        let x = mutex.lock();
        if x.check_something() {
            x.do_some_calculations();
            more = false;
        }
    }
    if more {
        // do different expensive calculations
    }

Probably something better than the above.

In Rust blocks also are expression so it can yield some value.

let cond = {
    let x = mutex.lock();
    if x.check_something() {
        x.do_some_calculations();
        false
    } else {
        true
    }
};
if cond {
    do_something_else();
}

BINGO! Yes. I wanted to use a block like that but some how failed to get rustc to like what I did. Thanks.

I was trying to do it without the need for the intermediate "cond" variable.

Something like :

     if {
        let x = mutex.lock();
        if x.check_something() {
            x.do_some_calculations();
            false
        } else {
            true
        }
    } {
        do_something_else();
    }

On a similar note, I recently created this monstrosity:

let best_combined: Arc<RwLock<Option<MyStruct>>> = Arc::new(RwLock::new(None));
... // rest of stuff happens in other thread
let current_best = best_combined.read().unwrap();
if /* some conditions */) {
       drop(current_best);
       let mut current_best = best_combined.write().unwrap();
       if /* exactly the same conditions as first if */ {
                               /* modify best_combined */
....

Is there any way to make it less repetitive and avoid explicit drop?
Thanks

If you use parking_lot's RwLock there is an .upgradable_read() guard you can get, to later upgrade() it.

1 Like

Note that in C++ every object that can be moved from has to have a "valid, but unspecified" moved-from state, so C++ doesn't need drop flags to keep track of validity because the drop flag is part of the type. It's also not necessarily true that moving out of something twice is a no-op; you still have to construct an "empty" object to put in the temporary, because it will be dropped. Chances are good this is cheap but not free.

Rust doesn't work like that. Moved-from objects are like destroyed objects: invalid and inaccessible. A major advantage to this is that all types can have move semantics, not just ones that have some kind of valid "empty" state. A side effect is that the compiler sometimes has to emit drop flags to know whether a thing has been moved-from or not (but note that drop flags are part of the variable, not part of the type like in C++ -- you can't move a value out of the middle of a Vec and leave an "empty" object in its place).

It's because of this different model that drop flags are kind of implicit and hidden from the programmer. Drop flags aren't really an explicit feature of variables; they're just a consequence of the scope rules, and they are frequently optimized out entirely. There's no syntax that would allow you to check drop flags because that would break the abstraction. Of course, if you really need to, you can mem::replace(&mut foo, Default::default()), which is the spiritual equivalent to "moving out of" a C++ variable, and works for anything that is Default. And if your variable isn't Default, you can wrap it in Option. But that's really not something that comes up that often. Like I said in my first post in this thread, the usual way to end a value's life is to make it go out of scope. That is how you use drop flags. I can't think of a scenario where being able to check drop flags would help, that can't be trivially rewritten to use scopes -- can you?

4 Likes

In theory, there may be a code that is hard to rewrite using scopes:

struct HugeStruct {}

impl Drop for HugeStruct {
    fn drop(&mut self) {
        println!("drop");
    }
}

// Signature of f() can't be changed.
fn f(x: HugeStruct, y: HugeStruct) {
    let z = match 1 < 2 {
        true => x,
        false => y,
    };
    // How to drop unused value here?
    // drop(x);
    // drop(y);
    println!("doing heavy job on z");
}

Btw, after rethinking the problem, I understood what I'd want from Rust: if some value is dropped (consumed) in one branch, it should be implicitly dropped in another, i.e. the following code samples should be equivalent:

if some_cond {
    some_code_that_consumes(x);
}
if some_cond {
    some_code_that_consumes(x);
} else {
    drop(x);
}

That way:

  • Resources are freed ASAP.
  • No need in runtime drop flags.
  • Each piece of code is always guarded by some set of values, that is known at compile time.
  • No values live but cannot be accessed due to borrowing rules.

If the compiler automatically inserted a drop in the else branch like that, it would be impossible to express something like @2e71828's example, where one branch needs to keep the lock longer than the other.

Also it would break code like this:

fn parse_or_push_with_default(x: String, errors: &mut Vec<String>) {
    let r: &str = match x.parse::<i32>() {
        Ok(_) => &x,         // get a reference that borrows from x
        Err(_) => {
            errors.push(x);  // move out of x
            "10"             // and get a reference that doesn't borrow it
        }
    };
    // r now refers to a &str that contains a valid i32. It might point to
    // x, or it might not. Drop flags enable this.
    println!("{}", r);
}

in which the borrow checker relies on x being left alive in the Err branch in order to prove that r is always valid when used.


Just realized I forgot to address your "hard to express" example, but @marcianx covered it for me :slight_smile:

2 Likes

Those code samples can't be made equivalent backward-compatibly, but there's no reason you can't do this yourself, even without scopes:

fn f(x: HugeStruct, y: HugeStruct) {
    // Since both x/y are dropped or moved in each branch,
    // no drop flags should remain in the final binary.
    let z = match 1 < 2 {
        true => {drop(y); x},
        false => {drop(x); y},
    };
    println!("doing heavy job on z");
}

Also, if you like, you can still use scopes in this specific case by moving the parameters into a scope:

fn f(x: HugeStruct, y: HugeStruct) {
    let z = {
        let x = x;
        let y = y;
        match 1 < 2 {
            true => x,
            false => y,
        }
    };  // The unmoved x/y is dropped here and
    // the compiler should be smart enough to
    // not require run-time drop flags by making
    // it roughly equivalent to the version above.
    println!("doing heavy job on z");
}
3 Likes

You can even let Rust figure out which vars are to be dropped by using a non-move (to ensure minimal owned captures) closure:

fn f(x: HugeStruct, y: HugeStruct) {
    let z = (|| {
        /* no need to let x = x; ... */
        match 1 < 2 {
            true => x,
            false => y,
        }
    })();  // The "unmoved" x/y is dropped here
1 Like