capturing in closures in Rust '21
Asked Answered
A

1

12

I just found out that the following code compiles in Rust 21 (used to not compile in 18)

fn get_func (i: &mut i32) -> impl Fn() -> i32 + '_ {
    || *i
} 

Is there an implicit move of i involved ? If so, then why does the following code compile too?

fn get_func (i: &mut i32) -> impl Fn() -> i32 + '_ {
    let f = || *i;
    
    println!("{:?}", i);  // was expecting here to give borrow of moved variable error, `&mut` doesnt implement `Copy` trait
    
    f
}

Or instead is it implicitly moving (copying in this case) the value being pointed to? But then the following code should compile, which doesn't -- indicating it's moving the reference.

fn get_func (i: &mut i32) -> impl Fn() -> i32 {
    || *i
}
Anthe answered 7/11, 2021 at 6:27 Comment(4)
I think that there is not an implicit move, but an implicit non-mutable reborrow, somewhat as if you wrote: let f = { let ii = &*i; move || *ii };. but I cannot find this new behavior documented anywhere... Maybe from rfc/2229: References to structs would be made more precise by reborrowing fields...Harriman
@Harriman An implicit reborrow would explain it. Since the closure doesn't need a mutable but a shared reference, it also explains (along with an equivalent reborrow performed by println!()) how println!() can access the mut reference i while f refers to it. Both the closure and println!() are reborrowing, and thus downgrading the mutable reference to two shared referenes. This is allowed, similar to how it's allowed to invoke foo(&x.foo, &x.bar) where x is &mut FooBar. play.rust-lang.org/…Airedale
Looks more like syntactic sugar. Move can be omitted in the 2021 version. With move || *i, the example is compiled and behaves in the same way with versions 2018 and 2021.Hobgoblin
Yes, it seems more to be implicitly moving i, instead of reborrow. In the second example, it is smart to "defer" the move to the end of the function.Anthe
B
2

It's downgrading your &mut to a & and making as many copies as it needs to.

This is all a byproduct of the fact that &mut has all the permissions of &; in other words it's fine to downgrade from a mutable reference to a non-mutable reference, and it's fine to copy a non-mutable reference as many times as you want, so this is just doing both of those implicitly.

fn get_func (i: &mut i32) -> impl Fn() -> i32 + '_ {
    *i += 1; // fine, still mutable
    let f = || *i;
    println!("{}", *i); // fine, no mutability required
    *i += 1; // ERROR: cannot mutate *i while borrowed by f
    f
}

It's worth noting that if you actually do try to capture it as mutable, this happens.

fn get_func (i: &mut i32) -> impl FnMut() -> i32 + '_ {
    println!("{}", *i); // Fine
    *i += 1; // Fine
    let f = || {*i += 1; *i};
    println!("{}", *i); // ERROR: cannot borrow *i as immutable
    f
}

Also the lifetime doesn't refer to the return of the closure, but the closure itself. Because no memory is moved, the closure is only valid so long as the memory at i's address remains valid. That's why removing the lifetime makes the code not compile.

In any case, all you really have to remember is the classic: "one mutable reference OR multiple immutable references, not both".

Beseem answered 13/2, 2023 at 11:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.