Drop semantics and temporary value lifetimes in while let

while let $pat = $expr
    $block

is the same as

loop {
    if let $pat = $expr
        $block
    else {
        break;
    }
}

So this is not really about while let and loop being different; it is about let $pat = $expr and if let $pat = $expr (or equivalently a match binding) being different / having different semantics.

So let's go back to a more minimal example, getting rid of loops since they don't play any role:

match vs. let

First of all, let's set up the stuff that replicates a mutex guard behavior:

  • mutex.lock() constructs a value with drop glue, i.e., that mem::needs_drop():

    #[derive(Default)] // default() <=> mutex.lock()
    struct WithDropGlue;
    impl Drop for WithDropGlue { fn drop (&mut self) {} }
    
  • value.recv() borrows this value to, in turn, create an "owned thing", i.e., something that is : 'static (hence value could be dropped while this thing exists without causing any issue whatsoever):

    impl HasDropGlue {
        fn recv (self: &'_ Self) -> () {}
    }
    

match case

fn foo ()
{
    //    |-- mutex.lock() ----|
    match HasDropGlue::default().recv() { _smth => {
        match_body();
    }}
}

gives the following MIR (with panic = abort to remove unwind paths):

The temporary is indeed only dropped at the end of the whole match expression

let

fn foo ()
{
    {
        //          |-- mutex.lock() ----|
        let _smth = HasDropGlue::default().recv();
        body();
    }
}

The temporary is dropped right after the let assignment

Summary

fn foo ()
{
    //    |-- mutex.lock() ----|
    match HasDropGlue::default().recv() { _smth => {
        match_body();
    }} // drop::<HasDropGlue>(__guard__)
}

fn foo ()
{
    {
        //          |-- mutex.lock() ----|
        let _smth = HasDropGlue::default().recv();
        // drop::<HasDropGlue>(__guard__)
        body();
    }
}

what if revc() had returned something tied to the '_ input lifetime ?

Then the the match version would still work, with a temporary held alive as long as needed; whereas the classic let version would fail to compile with a "temporary does not live long enough" error.

  • Note that when raw pointers are involved, such as with CString::new().as_ptr(), Rust does not see the borrow and can thus lead to a use-after-free unsoundness.

What to make of all this

This is one of the "quirks" of Rust, which proves that if explicit RAII semantics are wanted (such as with lock guards), then anonymous temporaries should be avoided. You may submit an RFC to change this, to get a change of temporaries drop placement, but know that:

  1. this would require, at least, a new edition

  2. Having program semantics change with an edition change would be very weird (why should a line in Cargo.toml change the semantics of a program without a compilation error? c.f. CString::new().as_ptr())

Thus this behavior cannot be changed.

On the other hand, I can see the issue of this being a particularly obscure thing for Rust, which goes against its "explicit semantics" philosphy: I thus think that filing an RFC for a warning lint against temporaries with drop glue in match would be a great thing to do, and if it fails, the lint could be added to clippy instead.

  • Actually, it would be great if, in a similar fashion to #[must_use] types, we could have #[time_sensitive_drop] or #[must_drop_explicitely] types.