Lifetime of a temporary value at the end of a block

I ran across an interesting error from the borrower checker that I encountered while refactoring a trait to make it object safe. One of the changes I made was to convert a generic parameter for the trait Iterator<Item = &[u8]>:

fn verify<'a, I: Iterator<Item = &'a [u8]>>(&self, parts: I, signature: &[u8]) -> Result<()>;

to an object parameter of type &mut dyn Iterator<Item = &[u8]>:

fn verify(&self, parts: &mut dyn Iterator<Item = &[u8]>, signature: &[u8]) -> Result<()>;

I decided to use a mutable borrow so as to allow the caller to pass a reference to a stack allocated object. This necessitated adding &mut in front of the parts argument at every call site for verify, but that wasn't too bad. The interesting thing occurred when I updated this test:

fn sign_verify_test(creds: &TpmCreds) -> Result<()> {
        let data: [u8; 1024] = rand_array()?;
        let parts = [data.as_slice()];
        let sig = creds.sign(parts.into_iter())?;

        creds.verify(parts.into_iter(), sig.as_slice())
    }

to this:

fn sign_verify_test(creds: &TpmCreds) -> Result<()> {
        let data: [u8; 1024] = rand_array()?;
        let parts = [data.as_slice()];
        let sig = creds.sign(parts.into_iter())?;

        creds.verify(&mut parts.into_iter(), sig.as_slice())
    }

(the only difference is that &mut is present in the first argument to verify in the second version). This resulted in a borrow checker error:

error[E0597]: `data` does not live long enough
    --> crates/btlib/src/crypto/tpm.rs:1656:22
     |
1655 |         let data: [u8; 1024] = rand_array()?;
     |             ---- binding `data` declared here
1656 |         let parts = [data.as_slice()];
     |                      ^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
1659 |         creds.verify(&mut parts.into_iter(), sig.as_slice())
     |                           ----------------- a temporary with access to the borrow is created here ...
1660 |     }
     |     -
     |     |
     |     `data` dropped here while still borrowed
     |     ... and the borrow might be used here, when that temporary is dropped and runs the `Drop` code for type `std::array::IntoIter`
     |
     = note: the temporary is part of an expression at the end of a block;
             consider forcing this temporary to be dropped sooner, before the block's local variables are dropped
help: for example, you could save the expression's value in a new local variable `x` and then make `x` be the expression at the end of the block
     |
1659 |         let x = creds.verify(&mut parts.into_iter(), sig.as_slice()); x
     |         +++++++

Following the compiler's suggestion did indeed resolve the error, but I wanted to really understand what's happening here.

My understanding is that the temporary struct created by parts.into_iter() is being dropped at the end of the block, and since this struct contains a reference to data, which is being dropped at the same point, there is an error because borrowed data does not live strictly longer than the borrower. My guess as to why the compiler's suggestion solves the problem is that in the modified code, the temporary is dropped at the end of the expression, which is before the point where data is dropped, so there is no issue. Is this reasoning correct?

If this is what's going on, I'm a bit surprised. I would've expected the compiler to drop the temporary at the end of the expression in which it occurs, even if this expression occurs at the end of a block. Is there a reason why the compiler doesn't do this?

Temporaries are dropped at the end of the statement that they occur in. If the temporary is created in the trailing expression of a block, the "statement" they are in is the statement the block is in, because the value of the expression becomes the value of the block and thus must outlive the block.

So, the compiler's suggestion fixes this by putting the temporary in a separate statement that ends before the end of the block.

4 Likes

FYI this also works and is nicer than the compiler suggestion IMO.

    let mut iter = parts.into_iter();
    verify(&mut iter, sig.as_slice())
1 Like

another way to make the statement out of the tail position is:

creds.verify(&mut parts.into_iter(), sig.as_slice())?;
Ok(())
1 Like

Ok, so it's the end of a statement that marks the end of a temporary's lifetime. But, I find it odd that the temporaries created in the expression at the end of a block are required to outlive the block. What's actually needed is for the value the expression evaluates to to outlive the block, not the temporaries that may have been created to compute it.

Based on the last comment here, you could create some issue to track tail expression temporary scope specifically. Niko was probably thinking of this issue. There are a few other issues filed about it. There's also this old unimplemented(?) RFC, but I didn't take the time to figure out how relevant it is.

There are cases where it is necessary for the temporary to outlive the block; it's possible for the block's value to borrow from that temporary. I can't easily think of a useful example, but this rather silly code is an example:

fn main() {
    let x = {
        String::from("hello").as_str()
    }.to_string();
    println!("{x}")
}

The block evaluates to an &'a str, where 'a is the lifetime of the temporary String value. If the temporary's lifetime ended at the end of the block then the borrow checker would have to reject this code, but this does actually compile: the temporary lives until the ; and so we can call to_string() on it successfully and satisfy the borrow checker that everything is okay.

Before this discussion, if someone had shown the code in your previous post and asked me if it compiles, I would have told them no. My false intuition was that the String was dropped at the end of block and so the &str produced by the block would outlive the String it references. Apparently I'm not the only one who found the actual semantics of the language unintuitive because several other people opened issues about this.

However, your code example shows that changing the scope of the temporary would be a breaking change (unless the compiler can figure out when extending the scope of the temporaries is required, but adding rules like that to the language can make it overly complicated). Because it's trivial to introduce a new statement to limit the scope of a temporary, making a breaking change doesn't seem worth it.

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.