2
votes

I need a closure to refer to parts of an object in its enclosing environment. The object is created within the environment and is scoped to it, but once created it could be safely moved to the closure.

The use case is a function that does some preparatory work and returns a closure that will do the rest of the work. The reason for this design are execution constraints: the first part of the work involves allocation, and the remainder must do no allocation. Here is a minimal example:

fn stage_action() -> Box<Fn() -> ()> {
    // split a freshly allocated string into pieces
    let string = String::from("a:b:c");
    let substrings = vec![&string[0..1], &string[2..3], &string[4..5]];

    // the returned closure refers to the subtrings vector of
    // slices without any further allocation or modification
    Box::new(move || {
        for sub in substrings.iter() {
            println!("{}", sub);
        }
    })
}

fn main() {
    let action = stage_action();
    // ...executed some time later:
    action();
}

This fails to compile, correctly stating that &string[0..1] and others must not outlive string. But if string were moved into the closure, there would be no problem. Is there a way to force that to happen, or another approach that would allow the closure to refer to parts of an object created just outside of it?

I've also tried creating a struct with the same functionality to make the move fully explicit, but that doesn't compile either. Again, compilation fails with the error that &later[0..1] and others only live until the end of function, but "borrowed value must be valid for the static lifetime".

Even completely avoiding a Box doesn't appear to help - the compiler complains that the object doesn't live long enough.

1

1 Answers

3
votes

There's nothing specific to closures here; it's the equivalent of:

fn main() {
    let string = String::from("a:b:c");
    let substrings = vec![&string[0..1], &string[2..3], &string[4..5]];
    let string = string;
}

You are attempting to move the String while there are outstanding borrows. In my example here, it's to another variable; in your example it's to the closure's environment. Either way, you are still moving it.

Additionally, you are trying to move the substrings into the same closure environment as the owning string. That's makes the entire problem equivalent to Why can't I store a value and a reference to that value in the same struct?:

struct Environment<'a> {
    string: String,
    substrings: Vec<&'a str>,
}

fn thing<'a>() -> Environment<'a> {
    let string = String::from("a:b:c");
    let substrings = vec![&string[0..1], &string[2..3], &string[4..5]];
    Environment {
        string: string,
        substrings: substrings,
    }
}

The object is created within the environment and is scoped to it

I'd disagree; string and substrings are created outside of the closure's environment and moved into it. It's that move that's tripping you up.

once created it could be safely moved to the closure.

In this case that's true, but only because you, the programmer, can guarantee that the address of the string data inside the String will remain constant. You know this for two reasons:

  • String is internally implemented with a heap allocation, so moving the String doesn't move the string data.
  • The String will never be mutated, which could cause the string to reallocate, invalidating any references.

The easiest solution for your example is to simply convert the slices to Strings and let the closure own them completely. This may even be a net benefit if that means you can free a large string in favor of a few smaller strings.

Otherwise, you meet the criteria laid out under "There is a special case where the lifetime tracking is overzealous" in Why can't I store a value and a reference to that value in the same struct?, so you can use crates like:

owning_ref

use owning_ref::RcRef; // 0.4.1
use std::rc::Rc;

fn stage_action() -> impl Fn() {
    let string = RcRef::new(Rc::new(String::from("a:b:c")));

    let substrings = vec![
        string.clone().map(|s| &s[0..1]),
        string.clone().map(|s| &s[2..3]),
        string.clone().map(|s| &s[4..5]),
    ];

    move || {
        for sub in &substrings {
            println!("{}", &**sub);
        }
    }
}

fn main() {
    let action = stage_action();
    action();
}

ouroboros

use ouroboros::self_referencing; // 0.2.3

fn stage_action() -> impl Fn() {
    #[self_referencing]
    struct Thing {
        string: String,
        #[borrows(string)]
        substrings: Vec<&'this str>,
    }

    let thing = ThingBuilder {
        string: String::from("a:b:c"),
        substrings_builder: |s| vec![&s[0..1], &s[2..3], &s[4..5]],
    }
    .build();

    move || {
        thing.with_substrings(|substrings| {
            for sub in substrings {
                println!("{}", sub);
            }
        })
    }
}

fn main() {
    let action = stage_action();
    action();
}

Note that I'm no expert user of either of these crates, so these examples may not be the best use of it.