Question on _ binding

I've read (and verified) that let _ = .. binding does not really bind the RHS to LHS and can be considered a no-op. Particularly, no copy/move of RHS happens due to such assignment.

However, there is some behaviour I cannot explain: when I have a move closure that performs an _ binding, compiler sees it as a move; however, the value is not dropped from the closure scope.

#[test]
fn test_underscore() {
    struct A(usize);
    impl Drop for A {
        fn drop(&mut self) {
            println!("dropping instance {}", self.0);
        }
    }

    let a1 = A(1);
    let clousure = move || {
        let _ = a1;
        println!("called clousure")
    };
    clousure();
    let _a1 = a1; // <- move error here
    let _a2 = A(2);
}

This produces following error, proving that closure takes the ownership of a1


However, if I comment out the line that produces error (reuse of a1), then I expect a1 to be dropped before a2 is dropped (because it has been moved to a scope that ends first; but that does not happen

#[test]
fn test_underscore() {
    struct A(usize);
    impl Drop for A {
        fn drop(&mut self) {
            println!("dropping instance {}", self.0);
        }
    }

    let a1 = A(1);
    let clousure = move || {
        let _ = a1;
        println!("called clousure")
    };
    clousure();
    // let _a1 = a1;
    let _a2 = A(2);
}

Could someone please explain what is going on here?

Using _ will still take ownership of the value and destroy it. It's equivalent to this:

drop(a1);

The reason that it behaves different to e.g. let _var = ... is that a lone underscore is a pattern that means "destroy the value".

Edit: I see that there is more to this question. I'm not sure why the println in the closure comes first either.

I agree with the poster's description of what _ does.

See this code playground link: _ does not move and does not take ownership, and does not borrow.

fn main() {
    let mut s = String::new();
    let _ = s;
    drop(s);
}
3 Likes

Why shouldn't A(2) drop before A(1) does?

Both should drop at the end of the test_underscore function if I understand correctly.

Consider that A(1) is a capture of closure. Basically it will work like it's stored in a struct field of the closure value. So it should drop when the closure value goes out of scope.

The actual let _ is a red herring and the thing that matters is that a1 is mentioned in the closure, and it's a move closure.

At the end of test_underscore, the latest added bindings in the scope drop first: first _a2, then closure.

3 Likes

But shouldn't this closure be an impl FnOnce, which is dropped right after being called, due to A(1) being moved into the _ binding?

I was thinking that because A(1) is moved inside the scope of closure, A(1) will be dropped when the closure runs and the scope where move happens has ended. But probably that is not how rust generates the implicit struct for the closure.

I tried putting braces around closure definition to limit the scope of the closure, and that indeed makes A(1) drop first - supporting what you hinted.

So the question remaining for me is, why is let _ = ... binding considered a valid use of A(1) inside a move closure, whereas identical statement will be a no-op if written outside the closure.

Actually not in this case, it literally does nothing. The reason why it usually act as drop is that it's a statement and any temporary values created from expressions are dropped at the nearest enclosing statement is done if not already owned by something else. In this case the value is owned by the variable a1 so it will not be dropped. Since it's not consumed on execution you can even call the closure multiple times.

But it doesn't affect the fact that the variable a1 is used within the move closure. The variable a1 is moved into the closure value closure and dropped when the closure dropped.

3 Likes

This all seems straightforward.

Using the move keyword moves the variable a1 as long as it is mentioned in the closure, so the variable is dropped when the closure is dropped.

Using the move keyword does not make the closure FnOnce and it can be called multiple times. Moving into a real variable will make the closure FnOnce. The traits a closure implements is dependent on what you do with the captured variables, not usage of the move keyword.

Example: Rust Playground

5 Likes

Thank you everyone for explaining this!

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.