Will references created by Rc::new be prematurely released?

Will references created by Rc::new be prematurely released? Why does commenting out the code result in a compilation error?

use std::{cell::RefCell, rc::{Rc, Weak}};

struct Node {
    parent: RefCell<Weak<Node>>,
}

fn main() {
    let leaf = Rc::new(Node {
        parent: RefCell::new(Weak::new()),
    });

    if let _ = leaf.parent.borrow() {

    }

     // comment next line, compiler will complain leaf no longer live? why?
     println!("why comment this println! leaf will no longer live?")
}

playground

2 Likes

I don't have time to explain in detail. It's mostly because the last expression in a scope has different lifetime semantics. If you replace the println with a semicolon (that turns the if into a statement), then it compiles too.

4 Likes

This is so weird. Here's a shorter example:

struct DropRef<'a>(&'a i32);

impl<'a> Drop for DropRef<'a> {
    fn drop(&mut self) {}
}

fn main() {
    let x = 5;
    if let DropRef(&4) = DropRef(&x) {}
}

I guess the condition of if-let is dropped after the block's local variables, but I don't see how that's useful. The solution in the error of adding ; seems like it would work in all situations.

Edit: Temporary lifetimes sometimes yield surprising errors · Issue #46413 · rust-lang/rust · GitHub

Huh. I wouldn’t have guessed that there’s a context in Rust in which a semicolon after a unit-typed block expression has semantic significance.

It's mostly because the last expression in a scope has different lifetime semantics.

do you know where i can find more detail about these? this is so interesting.

this is only for last expression

This is about the lifetime of temporary variables. The “rule of thumb” is that temporaries live until the end of the enclosing statement. However, the details are more tricky of course. This particular case is about return expressions from blocks.

Of course the rule that temporaries don’t get dropped immediately is generally useful

use core::cell::RefCell;

fn f(x: RefCell<Vec<u8>>) -> Option<()> {
    *x.borrow_mut().get_mut(10)? += 1;
    Some(())
}

you wouldn’t want the RefMut to be dropped so early you couldn’t do the += 1 without resorting to let mut br_mt = x.borrow_mut();. But what if you wrap something here into a block?

use core::cell::RefCell;

fn f(x: RefCell<Vec<u8>>) -> Option<()> {
    *{ x.borrow_mut().get_mut(10) }? += 1;
    Some(())
}

should the RefMut be dropped at those curly braces? But there isn’t an “enclosing statement” there… on the other hand, I personally wouldn’t mind if the rules had turned out that way.[1] Anyways… they haven’t, so the above still works. What if I introduce a statement to the block now?

use core::cell::RefCell;

fn f(x: RefCell<Vec<u8>>) -> Option<()> {
    *{
        let i = 5 + 5;
        x.borrow_mut().get_mut(i)
    }? += 1;
    Some(())
}

this still works! And I think that’s good for consistency, if the case before also works. But now that means the rule must be precisely that: temporaries in a block live up to the end of the statement in each statement, but they live longer (up to the end of the statement surrounding the whole block) for the return expression of the block.

Finally, the general rule is that the body of a function is just an ordinary block as any other... and thus it follows the same exact rules.


Another important factor here is the issue of optional semicolons after block-like expression statements. Usually, you don’t finish an if with a semicolon, because Rust has rules that a following expression/statement is automatically and implicitly separated. This has multiple fun consequences, one of them is that { EXPR } - EXPR at the top level of a block gets separated out into two statements implicitly, { EXPR }; -EXPR and one might need ({ EXPR }) - EXPR to get the other parsing. The other consequence is the one relevant here, that an if expression or block (or other block-like expression) as the final statement in a block is the return value, and not implicitly converted into a statement.

I think minimizing the confusion around this feature is also the motivation why this implicit ; only is allowed for expressions of type ().


The final puzzle piece of course is the temporaries of if let. While it looks similar to a let statement, an if let PAT = MATCHED_EXPR { CONTENTS }, there is no rule to limit the temporaries onside of MATCHED_EXPR, so its scope goes beyond the whole if let to the “surrounding statement”, or in this case, to the outermost temporary scope, “the entire function”, which is longer than the local variables in the function’s body live.


Temporary scopes are a bit of a mess IMO, but unfortunately, they are considered semantically relevant in Rust, so will hardly ever be changed, to keep stability. Besides lifetime errors[2], good temporary scopes can also be performance relevant: if drops coincide with control-flow, it helps avoid the need for dynamic drop flags[3]. The question of when destructors run can be relevant for low-latency code sections (you wouldn’t want to drop huge Vecs there unexpectedly). And keeping data around for too long can cause too much memory to be kept in-use. Last, but not least, unsafe code might rely on some guard object that, albeit not explicitly used, must not be dropped too early (e.g. before a critical section ends), which motivates predictable (and consistent) placement of drops[4].


  1. In fact, I believe it’s probably quite rare that you write code where you need this behavior, and it’s probably having fewer confusing consequences. ↩︎

  2. which can go either way, something dropped too early can conflict with a borrow of it, something with a destructor that’s containing a borrow can conflict with something else wanting to borrow or drop the same thing ↩︎

  3. which I believe is the motivation for entries in the temporary scopes rules such as the bodies of if/else, the arms of match, and the RHS of lazy boolean operators &&/||. If you’re unsure what drop flags are see here ↩︎

  4. though honestly more-so for local variables; one wouldn’t commonly use such guards with temporaries, I believe ↩︎

9 Likes

thanks.

There's a draft RFC to change the rules across an edition.

And here's a related blog post more focused on the super let idea, but quite in-depth on the temporary lifetime topic more generally.

3 Likes