Why is drop called after the match block?

In the following code "in match" is outputted before "drop". I naively expected that it would be the other way around because the scope of the temporary "Foo" would end before entering the match block. Can someone explain this behaviour?

Playground

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

fn foo(_f: &Foo) -> Option<()> {
    Some(())
}

fn main() {
    println!("start");
    match foo(&Foo) {
        _ => println!("in match"),
    }
    println!("end");
}

Temporaries from the expression that you match on live until the end of the whole match ... {} expression. The rules about when temporaries are dropped are in-deed a bit arbitrary; for match this can be important in cases where the temporary is borrowed and that borrow is used inside of the match. But this is inconsistent with e.g. if expressions, for example try this:

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

fn foo(_f: &Foo) -> Option<()> {
    Some(())
}

fn main() {
    println!("start 1");
    if matches!(foo(&Foo), Some(_)) {
        println!("inside 1");
    }
    println!("end 1");
    println!("start 2");
    if let Some(_) = foo(&Foo) {
        println!("inside 2");
    }
    println!("end 2");
}

(playground)

Outputs:

start 1
drop
inside 1
end 1
start 2
inside 2
drop
end 2

If you want to try to learn the rules in detail, the Rust reference would be your go-to source.

2 Likes

I think it’s informative to compare this behavior vs a version of foo() that takes Foo by value, where drop happens before “in match”. A match expression has to last as long as the match block in case you make bindings to the value (in this case derived from a reference to a temporary), although it does seem like in this specific case there’s no way for a value tied to the temporary’s lifetime to be bound in the match arms.

Here’s another fun tweak to your existing code that leads to drop before “in match”.

let v = foo(&Foo);
match v {
  // ... as before
}
1 Like

Here's a modification of your example to illustrate such a binding.

(Note that in a more complicated match, multiple arms could use bindings.)

1 Like

Makes sense. I just assumed that the compiler could prove that the type of the expression does not borrow the temporary.

Yes. This is the change I made once I identified what is causing my deadlock :smiley: The type in question was a lock guard, unfortunately.

Oh, I would personally love if it did something like this. The common counter-argument is that people want drops to be very predictable because things like e.g. using unsafe mutex-like APIs become hard to manage if you aren’t sure anymore when the lock is actually dropped (and thus released). In my opinion we would need language features that allow such an API to force the user to use an explicit drop call where they expect the drop to happen. IMO a huge improvement over the current situation where, as you have noticed with your example, the current, predictable rules aren’t always 100% intuitive. But I guess that’s to radical a change to happen any-time soon.

Also on needs to keep in mind that changing when destructors are called in an observable manner on any existing code is technically a breaking and Rust likes to be very stable.

Yeah that would be a very nasty subtle change to introduce. Even when guarded behind an edition it would be a massive PITA to port existing code to.

There was some related discussion about temporary lifetime inconsistency in this recent thread:

1 Like

I certainly think it would be a bad idea to let drop locations depend on type information such as lifetimes.

I wouldn't think it's that negative. When you use Rcs, the drop location even depends on runtime information. I don't like implicit drops that have very relevant visible effects anyways, and other situations, e.g. where drop just does deallocation, the worst effect you can get from a surprising time of drop is bad latency.

I guess if things go that way, drop(x) can always be used to delay a drop by extending lifetime where it matters as easy as it can shorten a lifetime currently.

std::mem::drop is your best friend, if you want to ensure something is dropped at a certain location. I do use it for pragmatics, sometimes, e.g. when constructing an instance from a raw pointer and I only construct it to drop it, afterwards.

The drop locations of each specific Rc is determined at compile time. What depends on runtime-information is what happens inside the destructor, but I don't think its comparable at all.