Why is Ref from cell.borrow().clone() extended in if let/match/match with binding/for but dropped in if/while?

cell.borrow().clone() creates a Ref temporary and returns an owned value. The Ref should be droppable immediately afterclone() since the owned value doesn't borrow from it.

However, if let/match/match with binding/for extend the Ref lifetime to the entire construct, while if/while drop it after evaluating the expression.

Construct Ref behavior
if Dropped
while Dropped
for Extended
match Extended
if let Extended
match with binding Extended

The attached code demonstrates this inconsistency. So my question is: Why is Ref from cell.borrow().clone() extended in if let/match/match with binding/for but dropped in if/while? Is this a known issue, a bug or an intended feature?

Edit: I tested this with Rust 1.89

Thanks!

use std::cell::RefCell;

fn main() {
    println!("=== FOR LOOP (iterator expression) ===");
    run_test("for loop", for_loop_case);

    println!("\n=== IF STATEMENT (condition) ===");
    run_test("if statement", if_case);

    println!("\n=== MATCH STATEMENT (scrutinee, no binding) ===");
    run_test("match statement", match_case);

    println!("\n=== WHILE LOOP (condition) ===");
    run_test("while loop", while_case);

    println!("\n=== IF LET (scrutinee with binding) ===");
    run_test("if let", if_let_case);

    println!("\n=== MATCH WITH BINDING (scrutinee with binding) ===");
    run_test("match with binding", match_binding_case);
}

fn run_test(name: &str, f: fn()) {
    let result = std::panic::catch_unwind(f);
    if result.is_err() {
        println!("  {name}: PANICS (temporary extended)");
    } else {
        println!("  {name}: OK (temporary dropped)");
    }
}

fn for_loop_case() {
    let cell: RefCell<i32> = RefCell::new(3);

    // cell.borrow() returns Ref<i32>
    // .clone() returns OWNED i32
    // Ref should be droppable, but is extended to entire loop
    for _item in 0..cell.borrow().clone() {
        *cell.borrow_mut() += 1;
    }
}

fn if_case() {
    let cell: RefCell<i32> = RefCell::new(3);

    // cell.borrow() returns Ref<i32>
    // .clone() returns OWNED i32
    // Ref should be droppable after condition is evaluated
    if cell.borrow().clone() > 0 {
        *cell.borrow_mut() += 1;
    }
}

fn match_case() {
    let cell: RefCell<i32> = RefCell::new(3);

    // cell.borrow() returns Ref<i32>
    // .clone() returns OWNED i32
    // Ref should be droppable after scrutinee is evaluated
    let _ = match cell.borrow().clone() {
        3 => { *cell.borrow_mut() += 1; 1 }
        _ => 0
    };
}

fn while_case() {
    let cell: RefCell<i32> = RefCell::new(3);

    // Condition is re-evaluated each iteration
    // Ref should be dropped after each condition check
    while cell.borrow().clone() < 5 {
        *cell.borrow_mut() += 1;
    }
}

fn if_let_case() {
    let cell: RefCell<i32> = RefCell::new(3);

    // cell.borrow() returns Ref<i32>
    // .clone() returns OWNED i32, wrapped in Some()
    // Ref should be droppable after clone
    let _ = if let Some(_x) = Some(cell.borrow().clone()) {
        *cell.borrow_mut() += 1;
        1
    } else {
        0
    };
}

fn match_binding_case() {
    let cell: RefCell<i32> = RefCell::new(3);

    // cell.borrow() returns Ref<i32>
    // .clone() returns OWNED i32, wrapped in Some()
    // Ref should be droppable after clone
    let _ = match Some(cell.borrow().clone()) {
        Some(_x) => { *cell.borrow_mut() += 1; 1 }
        None => 0
    };
}

The drop scope of temporaries is determined syntactically, so whether or not the result of .clone() borrows the Ref or not can't effect the drop scope in cases such as your tests.

(The exact rules are sadly quite complicated due to things like temporary lifetime extension,[1] which includes special cases for & and &mut _ expressions. So in a sense some kinds of borrowing can effect drop scopes -- but those "kinds of borrowing" are syntactically defined.)

Presumably the reason is that all the extending cases create bindings (new variables). If the result of .clone() did need to borrow the Ref, the code wouldn't compile if the temporary drop scope was shorter than that of the binding.

if and while without let don't create bindings. The conditional expression always produces a non-borrowing bool.


Here's an article on the topic you may find interesting.

There have been some changes in a few details since that article.[2] But most of it is still relevant, and it discusses some of the "why"s.


  1. which extends the drop scope of temporaries, and not a '_ lifetime ↩︎

  2. Example: In edition 2024+, if let temporaries drop at the end of the first block. Before that they dropped after the else block. ↩︎