When locks or borrows are released (i.e. dropped)

Yes, in case that wasn’t clear, example #2 is definitely a case of temporary lifetime extension.

I didn't notice the error happened because of the println!. So the following compiles, but causes a runtime panic:

use std::cell::RefCell;
use std::ops::Deref;
fn main() {
    let cell: RefCell<i32> = RefCell::new(7);
    (cell.borrow_mut().deref(), cell.borrow_mut().deref());
}

(Playground)

That is, if I get it right, because the RefMut (as returned by borrow_mut()) is dropped at the end of the statement. (The second paragraph of "Temporary scopes" isn't restricted to place contexts, thus these RefMuts above both live until the statement and not until the end of the respective expression.) That is something I definitely need to keep in mind.

Yeah, I forgot about lifetime extension :confounded:.

Yeah, it was indeed the usage in println! that caused the problem.

:+1:

I think I understand now. Given the above, it makes sense that the shadowing removed the compile-time error, because the only remaining use was the case with the lifetime extension :dizzy_face:.

Agreed.

…not sure if I can think anymore… :woozy_face: I.e. I think I need to review the simpler examples first, and need to get some practice first, before trying to understand that part. But thanks for writing your thoughts down.

Also agreed.

For some reason I mistook the "place context" mention in the first paragraph of the "Temporary scopes" subsection to limit the whole subsection to place context use, but that's been simply wrong. Sorry if I caused confusion.

What I will try to remember (and hope to get it right now) is that any "lock" that I do not assign to an explicit variable will live at least until the end of the statement, unless I directly borrow from it within a let expression, in which case it may live until the end of the block (due to temporary lifetime extension). The exact rules of that temporary lifetime extension, however, are subject to change, and we believe (hope), that this won't change in a breaking manner without an edition change :innocent:.

While we’re on the topic of things to remember; there are surprising effects of the precise rules of temporary scopes: if expressions are different from if let expressions.

E.g. if you have something liket

use std::cell::RefCell;
fn main(){
    let x = RefCell::new(Some(0));
    
    if x.borrow().is_some() {
        *x.borrow_mut() = None;
    }
    
    println!("success");
}

– which works – and turn it into

use std::cell::RefCell;
fn main(){
    let x = RefCell::new(Some(0));
    
    if let Some(_) = *x.borrow() {
        *x.borrow_mut() = None;
    }
    
    println!("success");
}

or even

use std::cell::RefCell;
fn main(){
    let x = RefCell::new(Some(0));
    
    if let true = x.borrow().is_some() {
        *x.borrow_mut() = None;
    }
    
    println!("success");
}

or

use std::cell::RefCell;
fn main(){
    let x = RefCell::new(Some(0));
    
    match x.borrow().is_some() {
        true => {
            *x.borrow_mut() = None;
        }
        _ => {}
    }
    
    println!("success");
}

it will fail with a run-time error.

The reason being that for if expressions the temporaries get dropped after the evaluation of the predicate finishes, while for if let or match, the temporaries of the expression being matched stay around until the end of the whole if let / match expression.


No less confusing, while

use std::cell::RefCell;
fn main(){
    let x = RefCell::new(Some(0));
    
    if x.borrow().is_some() {
        *x.borrow_mut() = None;
    }
}

works fine,

use std::cell::RefCell;
fn main(){
    let x = RefCell::new(Some(0));
    
    if let Some(_) = *x.borrow() {
        *x.borrow_mut() = None;
    }
}

doesn’t even compile, while

use std::cell::RefCell;
fn main(){
    let x = RefCell::new(Some(0));
    
    if let Some(_) = *x.borrow() {
        *x.borrow_mut() = None;
    };
}

goes back to the runtime-error.

The compile-time error above has to do with the fact that temporaries of the return expression of a block belong to the surrounding temporary scope. This is why something like {foo.borrow_mut().deref()}.some_method() will still work (the temporary RefMut is not dropped at the end of evaluating the block). The body of a function is also a block in this regard.

These rules seem a bit pointless and stupid, but I guess they are what they are, so we’ll live with them for now.

1 Like

Yeah, I just noticed that too, when making this example:

use std::cell::RefCell;
use std::collections::HashSet;
fn main() {
    let cell = RefCell::new(HashSet::<i32>::new());
    if cell.borrow_mut().insert(1) {
        cell.borrow_mut().insert(2);
    }
}

(Playground)

This works fine for the reason you explained. Due to this from the reference:

Apart from lifetime extension, the temporary scope of an expression is the smallest scope that contains the expression and is one of the following:

  • The entire function body.
  • A statement.
  • The body of a if , while or loop expression.
  • The else block of an if expression.
  • The condition expression of an if or while expression, or a match guard.
  • The expression for a match arm.
  • The second operand of a lazy boolean expression.

Thus again, I was wrong, as a lock will not always live until the end of the statement. I guess the list of things to remember is a bit longer then. :rofl:

Thanks for these examples. I had to search for a while until I even noticed the difference between the last two!

Hmmm, I don't have enough practice to judge, but most rules seem to be quite useful. Anyway, I might want to keep my locks in a distinct variable (where I know when it's dropped), just to be sure.

P.S.: With the help of these rules, I could actually get rid of some variables in my projects that held locks, as the rules seem to help me in my cases (particularly the rule that the temporary lock can be dropped after the condition expression of an if has been evaluated). It feels a bit like operator precedence. You can sometimes make things shorter, but this might impede readability.

Maybe I was harsher than intended – the rules do of course have a point, they aren’t completely arbitrary. It’s still unfortunate that they also seem inconsistent at times (who’d expect the difference between if EXPR { ... } and if let true = still { ... }). It’s probably impossible to make up consistent rules that always seem to do the right thing. Maybe it would be better to just drop everything right after its last use (taking information from the borrow checker into account) instead of relying on syntactical rules only :thinking:

Sometimes you need to hold a lock (guard) even if it is not used. But that could be done with an explicit drop perhaps. I often wondered why automatic dropping always happens at the end of the "(drop) scope" and not before. But I'd not be surprised if there's a good reason for it.


Perhaps if the usage decides on how long locks are held, it could lead to surprising changes (when you touch your code) and thus surprising deadlocks, for example.

How would you build an RAII based scope guard that cleans up something unrelated (or invisibly related like through C-space) during a panic unwind as well as normal return?

I know that making handling (either unsafe or run-time-panicking) RAII things more predictable are a big reason Rust does it the way it does it. Of course there always would (have to) be some way to manually specify where things are dropped. I do not intend to discuss this idea too much further here, I just brought it up to reference a potential alternative that might be better, at least in some ways. (Of course it’s always a trade-off.)

It’s not a new idea either. There’ve been proposals e.g. to add a trait so that types can opt-in or perhaps opt-out of some “smart” earlier-dropping / eagerly-dropping behavior.

There’s also latency concerns. Maybe you want to send your HFT network response before deallocating and dropping that huge data structure that goes out of scope afterwards but already isn’t used anymore. Or maybe you don’t like blocking your whole application for a few seconds and you’d prefer to unlock that mutex before dropping the RAII struct that finishes up a file and writes it to disk when dropped, even though the last use of that RAII struct was right within that mutex’s critical section.

Dropping things earlier can introduce suprising additional latency to your code. OTOH, maybe the safest approach here would be to make points in the code where destructors have significant effects more visible and explicit.

But the issue at hand seems to be making drop behavior more predictable. I'm not sure any of these solutions will help much here.

Currently there's drop precision for anything with a location. Temporaries can be given a non-temporary location for convenience sometimes. I'm not sure what removing the drop precision for those values that do have an actual location will improve in this situation.

An opt-in EagerDrop trait would only move some of the values from the precise-drop group to the imprecise-drop group. You'd still have two groups except now them having a location doesn't necessarily tell you the whole story anymore.

Going the explicit route is basically introducing fully linear types, but that's been talked about for years, and tends to run into many issues and is a potentially huge change across the ecosystem. This one might be a solution, but I feel it's at least currently out of reach for Rust.

I would also argue on a more fundamental level that the drop precision we currently have is what enables the borrow checker to be flexible and evolve. Because you can always rely on the drop order of things to hold. No matter what type it is, if it's generic, if it's put in an Option or Vec, and so on. We'll also never need to worry about a composition of any of these situations behaving differently than we expect.

As a second thought, another way of going the explicit route instead of being linear might be to have a lint that allows one to catch and forbid temporary lifetime extension situations. Then you'd have to explicitly ensure everything that needs to stay alive has a location. With the lint active, all drops would be obvious as they follow normal scoping rules.

Would probably be a good lint to have, specifically for unsafe code, so you don't accidentally rely on something that isn't guaranteed.

I think catching temporary lifetime extensions might not be sufficient because even without these, the dropping rules can be very confusing/misleading. Temporary lifetime extensions are just one of many possibly confusing cases (I think).

One possible solution (not sure if this is feasible) would be to forbid any temporaries for values of types implementing Drop. That would force you to explicitly write

let guard = mutex.lock();

instead of just using it in an if-expression:

if mutex.lock().some_method() { /* lock isn't held here */ }

Note that the latter doesn't use temporary lifetime extensions. Yet it maybe surprising (if you don't know the precise rules) that the lock won't be held anymore in the block.

I don't want to argue in favor of forbidding to use .lock() in a condition expression, but just point out that a lint for temporary lifetime extensions might be insufficient for gaining extra safety here.

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.