How’s a capture in Rust closures?

vikram fugro
5 min readNov 6, 2021

--

We all know what a closure does and how powerful they are! They hold the special power of capturing the variables used within them but defined outside their own body’s scope. (i.e they can close over the scope larger than their own scope, hence the name — closure!).

Most of the modern day languages have them and Rust is no different. Let’s look at an example.

Here’s the C++ equivalent.

and the Go equivalent.

All the examples above compile fine and run as expected i.e they output 20. They all seem “safe” in what they are doing, as one can see factor lives for as long as the closure. It captures factor by reference, with the C++ one being a bit explicit in stating it —see [&] in the closure definition.

Now let’s get to the crux of what I mean by “safe” here — What if we need to return a closure from a function ? and that closure is capturing some local variable defined within the function it is returning from.

Here’s the Go example first:

This example works in Go as unlike Rust or C++, it has a garbage collector and the compiler does escape analysis. In short —it figures out factor should be placed on heap and not on stack, so that it can outlive the execution context of get_multiplier() and then later, when not in use can be swept off by the GC.

In C++, it’s a slippery slope! Here’s what I mean.

The above code would compile fine but give undefined result or even crash at runtime. The compiler won’t warn you about the variable factor being captured (via reference) by a closure that itself outlives factor (as factor lives on the stack of get_multiplier()). So you ought to be careful here and replace [&] by [=]. This will instruct the compiler to create a separate copy of factor for the closure.

Now let’s see what would happen in Rust.

The compiler will not compile the code above and following is the error you’d get:

error[E0373]: closure may outlive the current function, but it borrows `factor`, which is owned by the current function
--> src/main.rs:2:5
|
2 | |input: i32| {
| ^^^^^^^^^^^^ may outlive borrowed value `factor`
3 | input * factor
| ------ `factor` is borrowed here
|
note: closure is returned here
--> src/main.rs:1:35
|
1 | fn get_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `factor` (and any other referenced variables), use the `move` keyword
|
2 | move |input: i32| {
| ++++

For more information about this error, try `rustc --explain E0373`.

It says precisely what you just read above in the C++ case i.e closure is capturing the variable factor (by reference), but is outliving factor. By the time the closure multiplier is invoked inside main(), factor would have ceased to exist.

Isn’t this cool? Rust compiler does all this analysis for you to prevent any runtime surprises! It also instructs you to move the value into the closure. In this case, adding move will create a separate copy of factor for the closure.

Lifetimes and Closures in Rust

Let’s have a look at an example where a closure captures a reference.

This throws a couple of errors:

error[E0700]: hidden type for `impl Trait` captures lifetime that does not appear in bounds
--> src/main.rs:1:37
|
1 | fn get_multiplier (factor: &i32) -> impl Fn(i32) -> i32 {
| ---- ^^^^^^^^^^^^^^^^^^^
| |
| hidden type `[closure@src/main.rs:2:5: 4:6]` captures the anonymous lifetime defined here
|
help: to declare that the `impl Trait` captures '_, you can add an explicit `'_` lifetime bound
|
1 | fn get_multiplier (factor: &i32) -> impl Fn(i32) -> i32 + '_ {
| ++++

error[E0597]: `factor` does not live long enough
--> src/main.rs:3:17
|
2 | |input: i32| {
| ------------ value captured here
3 | input * factor
| ^^^^^^ borrowed value does not live long enough
4 | }
5 | }
| -
| |
| `factor` dropped here while still borrowed
| borrow later used here

Some errors have detailed explanations: E0597, E0700.

The return value of get_multiplier() is some concrete type that implements the Fn() trait. But the return value does not convey that it also captures a reference. Another way to look at this is that the compiler assumes that even if the closure does capture any references, all those references should be valid for 'static lifetime i.e for the entire duration of the program.

So basically, the compiler will desugar

fn get_multiplier (factor : &i32) -> Fn(i32) -> i32

to

fn get_multiplier<'a> (factor: &'a i32) -> impl Fn(i32) -> i32 + 'static

So you see, there’s a conflict! You can’t have a shorter lifetime 'a to be returned as a longer lifetime 'static . (Lifetimes are covariant), hence the compiler cries foul! So we need to fix this to:

fn get_multiplier (factor: &i32) -> impl Fn(i32) -> i32 + '_ , where the anonymous input lifetime (since there is only one input, otherwise we need to explicitly state it: <'a>) could be greater than or equal to the output lifetime '_.

Second error talks about the problem capturing the reference variable factor into the closure. Even though it is referencing some data (i32) whose (anonymous) lifetime is greater than the function get_multiplier(), the variable factor itself is only valid within the function get_multiplier(), i.e it lives on the stack. So it needs to be moved into the closure.

With the above two errors fixed, here’s the correct code:

This is the beauty of Rust — if it compiles fine, it runs fine!

That’s all for now. Hope this was helpful!

--

--

vikram fugro

Open Source Software Enthusiast, Polyglot & Systems Generalist.