Closure: capture by borrowing is not a regular reference?

As far as I know, variables in closure that hold captured values are pointers.

Q: what's the type of those variables that hold the captured value? Are they reference types (pointer)? Or the same type as the original value that's captured?

  1. if it's reference type: AFAIK, there's no auto-dereference in closure. Why I can use those variables without dereferencing?
  2. if it's the original value's type, how does rust do borrow check?

cited from the book

Closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter:

  1. taking ownership,
  2. borrowing mutably
  3. borrowing immutable.
fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

fn main() {
    let mut x = 10;
    let mut y = || {
        // shouldn't `x` be `&mut i32`?
        // *x += 10; not working here
        print_type_of(&x);
        x += 10;
        x
    };
    println!("{}", y());
    
    let z = &mut x;
    print_type_of(&z);
}

code. Output:

i32
20
&mut i32

Since x is captured in the closure, I would expect x to be a reference type because it's borrowed. But the reality is, x is a type of i32. And *x += 10; doesn't work. see code.

error[E0614]: type `{integer}` cannot be dereferenced
  --> src/main.rs:10:9
   |
10 |         *x += 10;
   |         ^^

Capturing involves some syntactic sugar which results in the captures having the same type inside the closure as the "original" variables outside. However, this is done in such a way that lvalue-ness is preserved, i.e. no copies are made, and only references are captured, but they are automatically dereferenced when the name of the corresponding variable is mentioned. You can think of it like this:

let ref_mut_x = &mut x;
let mut y = move || {
    *ref_mut_x += 10;
    *ref_mut_x
};

In principle, this is not any different from other forms of automatic dereferencing hidden behind syntactic sugar. For example, in the Index trait, the Index::index() method returns &Self::Output, but if you actually index into e.g. a Vec<i32>, using the some_vec[42] syntax, you'll get back an i32, not a &i32. This is because indexing is desugared to *Index::index(collection, index), i.e. it contains an implicit dereference operation, so that the types are what you would (naively) expect to be: directly the element type of the collection.

In both cases, the use of pointers/references is merely a technical necessity, an implementation detail which ensures that lvalues (place expressions) are preserved, so that no spurious copies or moves happen and mutation remains possible. Consequently, hiding this is usually not a problem.

3 Likes

Got it!

Thanks for letting me know. I can't find any mentions in the book. Can you point me a direction if I want to learn more about the implementation detail? I can only find docs for auto-dereference, which is only meant for method calls.

Much appreciated!

Here's one piece of docs I could find.

2 Likes

You can read more about how closures are desugared in mu article on the subject

1 Like

Finished reading your blog! It clearly written and well structured! Those examples are super helpful for learning the implementation details of rust closure.

Now because __closure_2__ doesn’t contain any lifetimes, it has a 'static lifetime, which is necessary for it to be sent across threads! But in doing so, we also lost Copy , now our closure in only Clone . :frowning:

good to know this.

Q: I'm a little bit confused about comment for the 4th example: (I figured out :smiley: )

// because this is a move closure, there are no new references here
let print_me = __closure_3__ { a_ref: a_ref };

When you say "no new references here", do you mean a_ref is moved into the closure struct (even it's trivially copied), instead of being borrowed as &a_ref? (yes)

let print_me = __closure_3__ { a_ref: &a_ref };

Wow! Will read! Thanks for your blog! :smiley:

Yes, exactly that

1 Like