Intermediate variables and `Drop`

Like the crocuses and tulips awakening from their winter slumber with the arrival of spring, questions about let-bound temporary lifetimes are perennials. (Or maybe it's more like zombies bursting from their graves—you decide.)

What language rules account for what's going on here?

struct PrintOnDrop(&'static str);

impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        print!("{}", self.0);
    }
}

fn main() {
    {
        // Prints "ab"
        let _ = PrintOnDrop("a");
        print!("b");
    }
    
    print!(" ");
    
    {
        // Prints "ba"
        let a = PrintOnDrop("a");
        let _ = a;
        print!("b");
    }
}

(playground)

This behavior is really surprising to me. Here's my shot at explaining what's going on; can anyone refute/confirm/undermine?

In let _ = ..., we're using the wildcard pattern _. Unlike an identifier pattern, a wildcard pattern "does not copy, move or borrow the value it matches" (reference). (I wasn't expecting that, but it makes sense...) So a statement like the second case's:

let _ = a;

does not move out of a. Indeed, a remains initialized until the end of its scope, and is dropped after printing "b", hence the output "ba".

In the first case, we say:

let _ = PrintOnDrop("a");

Again, the wildcard pattern does not take ownership of the value. This means that the rules about let-bound temporaries don't come into play, and the return value of PrintOnDrop("a") does not get assigned to a temporary location with the lifetime of the let's scope; it's just an ordinary subexpression whose value gets dropped immediately, as if you'd written { PrintOnDrop("a"); }.

(This bout of confusion is fallout from CAD97_'s tweet this morning.)

2 Likes

I don't remember where I saw this documented previously but if you change the first PrintOnDrop binding to:

let _a = PrintOnDrop("a");

then you might get what you expect?

1 Like

Here's a bonus -- actually let's make it two:

    {
        &PrintOnDrop("a");
        print!("b");
    }

    print!(" ");

    {
        let _ = &PrintOnDrop("a");
        print!("b");
    }

Playground for the answer.

2 Likes

So, despite the fact that the _ pattern is not going to do anything with the reference, Rust still puts the borrow-ee in a temporary with the lifetime of the let's scope? Is this because the simple syntactic rules don't take the wildcard pattern into account?

Oh, as always, I'm not concerned with actually getting anything done, I'm just wondering what Rust's rules are. Yes, using let _a retains the value until the end of the scope.

I don't know for sure, but it does seem like the temporary promotion happens regardless of the pattern.

2 Likes

我认为产生这个结果的原因是:

{
       // Prints "ab"
       let _ = PrintOnDrop("a");
       print!("b");
 }

在这个作用域内不存在对变量 _ 的引用,因此认为其为临时对象,因此会立即drop。反之,下一个则不是。

这里提供一个相应的例子。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=bd476f347cd30b4332d55008df444458


In addition, I first time reply post on the forum, and try to use Chinese that is can be accepted.

If you offend, please forgive me and tell me why. Thanks ... :rofl: :rofl: :rofl:

没关系
People who do not read Hanzi can use Google translate.

2 Likes

If I've understood the translation correctly, you're saying it's different since _ is unused.

I don't think this is the case, though? There is a substantive difference between _ and unused variables. _ is not a variable; it is defined to be the wildcard binding, which doesn't move/copy/bind any value.

See:

struct PrintOnDrop(&'static str);

impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        print!("{}", self.0);
    }
}

fn main() {
    {
        // Prints "ab"
        let _ = PrintOnDrop("a");
        print!("b");
    }
    println!();
    {
        // Prints "ba"
        let _a = PrintOnDrop("a");
        print!("b");
    }
}

(playground)

This makes variables like _lock useful when explicitly holding an unused mutex.

See also https://doc.rust-lang.org/reference/patterns.html#wildcard-pattern

2 Likes

I think your knowledge of the _ is correct. Thanks your reply.

BTW, it is amazing, you have understand my post ... :rofl: :rofl: :rofl: :rofl:

1 Like

Yikes! There have been times in the past where I've used let _ = x; when I really intended to write drop(x);. This is good to know!

Someone else said the same in a chat. Calling drop feels like it leaves the reader a shorter path of inferences to follow before they understand the meaning of the statement, so that's what I'd use.

(No idea how good a cognitive model that is for avoiding bugs...)

This behavior is not that surprising when you put it in the context of destructuring a composite object:


fn main() {
    let val = (PrintOnDrop("a"), PrintOnDrop("b"));
    
    {
        let (_, _b) = val;
        print!("c");
    }
}

playground

This prints cba, which is sensible because you're using the let binding to move from an aggregate, so it makes sense for the entire aggregate to be dropped together. Imagine if these are a pair of locks that need to be maintained in tandem. (You would need to be careful of the drop order, because the _ binding appears to always drop last, no matter which position it is in.)

The let _ = ... variation appears to be a special case of this.

2 Likes

But, the aggregate isn't dropped together:

fn main() {
    let val = (PrintOnDrop("a"), PrintOnDrop("b"));
    
    {
        let (_, _b) = val;
        print!("c");
    }
    
    print!("d");
}

playground

This prints cbda, showing that the remainder of the program runs between dropping b and a.

2 Likes

This was mentioned somewhere else, but what's notable about _ is that it doesn't bind. So,
let _ = PrintOnDrop("a"); makes rust immediately drop the newly created PrintOnDrop (it's essentially the same as not having a let _ = in the statement) and let _ = some_var; doesn't move some_var, like you would expect with a normal variable.

1 Like