rustlifetimeborrow-checker

Assigning RefCell method parameter to a local variable produces compile error


Consider the following simple example:

use std::cell::RefCell;

// Compiles fine
fn good(t: RefCell<String>) -> bool {
    t.borrow().len() == 12
}

// error[E0597]: `t` does not live long enough
fn bad(t: RefCell<String>) -> bool {
    let t = t;
    t.borrow().len() == 12
}

Playground

The bad function fails to compile with the following error:

error[E0597]: `t` does not live long enough
  --> src/lib.rs:9:5
   |
8  |     let t = t;
   |         - binding `t` declared here
9  |     t.borrow().len() == 12
   |     ^---------
   |     |
   |     borrowed value does not live long enough
   |     a temporary with access to the borrow is created here ...
10 | }
   | -
   | |
   | `t` dropped here while still borrowed
   | ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `Ref<'_, String>`
   |
   = 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
   |
9  |     let x = t.borrow().len() == 12; x
   |     +++++++                       +++

This looks extremely strange to me. Just reassigning function parameter to a local variables fails the compilation. Could you explain why?


Solution

  • This is mentioned in the Reference:

    Temporaries that are created in the final expression of a function body are dropped after any named variables bound in the function body. Their drop scope is the entire function, as there is no smaller enclosing temporary scope.

    I'm not sure if this is necessary for the language to function, makes the language more convenient, or is simply how the compiler was originally implemented, but it would be backwards-incompatible to change now. If someone knows more, please comment or edit this answer.

    I've been aware of this, and it comes up sometimes when understanding drop order. What's new to me is that function arguments, apparently, get dropped even later. This is almost mentioned here but not quite. The drop order thus must be:

    Here's a program that demonstrates that.

    struct A(u32);
    
    impl A {
        fn nothing(&self) {}
    }
    
    impl Drop for A {
        fn drop(&mut self) {
            println!("drop {}", self.0);
        }
    }
    
    fn f(_a1: A) {
        let _a3 = A(3);
        A(2).nothing()
    }
    
    fn main() {
        let a = A(1);
        f(a);
    }
    

    It prints the following:

    drop 3
    drop 2
    drop 1
    

    In your code, the final expression creates a temporary std::cell::Ref<'_, String>. Since Ref has Drop behavior—specifically, the lifetime is contained in a type that implements Drop—it requires that the lifetime is valid when drop occurs, unlike a normal shared reference &. Since it borrows from the RefCell<String>, the Ref must be dropped before the RefCell is dropped. And according to the above rules, this is not satisfied when RefCell is bound inside the function.

    The workaround in the error message should always be sufficient.