Underscore expression and the drop order

Hello everyone.

Rust got me bamboozled again.

So I have this code:

struct Droppable;

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

struct S {
    droppable: Droppable,
}

and two test functions:

fn test1() {
    println!("test1");
    let s = S { droppable: Droppable };
    {
        {
            let S { droppable: _ } = s;
        };
        println!("should be dropped?");
    }
    println!("end of test1");
}

fn test2() {
    println!("test2");
    let s = S { droppable: Droppable };
    {
        {
            let S { droppable: _droppable } = s;
        };
        println!("should be dropped?");
    }
    println!("end of test2");
}

The only difference between them is the destructuring of s: it's let S { droppable: _ } = s in the first case and let S { droppable: _droppable } = s in the second.

The second case behaves as I expect it to and droppable is dropped in the same block where s is destructured. I.e. i get

test2
dropping
should be dropped?
end of test2

But in the first case it's dropped at the end of the function.

test1
should be dropped?
end of test1
dropping

Here is the full code on playground.

This was very surprising and I actually had a bug because of it, because my real-world droppable needed to be dropped early.
But now I'm wondering why it works like this. I've read the chapter about destructors in the docs and couldn't find any explanation for this. Did I miss something?

I think it is due to the wildcard pattern being treated differently than identifier patterns when it comes to moving a value. If we use an identifier to assign the droppable value to a new variable, the value is moved to that variable and dropped when it goes out of scope. But if we use the wildcard pattern, the value isn't moved:

Unlike identifier patterns, it does not copy, move or borrow the value it matches.

~ Patterns - The Rust Reference

and thus droppable is dropped when s is, at the end of the function body scope.

1 Like

the wildcard pattern means "ignore", which actually does nothing when you bind some value to it: specifically it don't move the value.

I agree this is very suprising to new learners. I remember I was shocked the first time when I learned this snippet contains NO undefined behavior:

let null: *const u8 = std::ptr::null();
unsafe {
    let _ = *null;
}
1 Like

Thanks everyone. Now it does make sense indeed.

1 Like

I'm shocked. I understand you are saying that

let _ = *null;

is more like

*null;

and not like

let a = *null;

But I thought that having *null in itself is already UB. Is there a rule that referring to invalid locations is fine as long as we don't read or write?

For another example of this see the &raw syntax. You can say &raw mut *null and it is completely fine because it just computes a new pointer without dereferencing it.
This blog post by one of the rust developers explains the concept really well in my opinion.

The analogy doesn't work: *null; is UB because it reads the value and discards it. In order to get away with *null you need to not read it, and that means you must use it in a place context that does something other than reading it. A bare expression as a statement is in value context, not place context.

Wow, this is more involved than I thought. So the right side of = would be a place expression if and only if the left side is an _? I don't see where it says that, though in the link, though.

The right side of a let statement is always a place context. Whether that place is read from depends on what the pattern is.

1 Like

Right, thanks! It does list "The initializer of a let statement" as a place context. It's surprising at first, because the right side seems to be the classical case of an rvalue, but I understand we need it to be a location so that we can take a reference to it.