2

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
user4815162342
  • 141,790
  • 18
  • 296
  • 355

1 Answers1

3

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • The linked answer demonstrates the two-step creation of reference to itself. I thought it would work here, using a box to prevent moves. But [it doesn't compile, either](https://play.rust-lang.org/?gist=b51113c7b9e233f5c3518cf95da47fc7&version=stable&backtrace=0), and I'm not really sure I understand why - surely there is no move if the object is boxed? Or is it the case that the compiler cannot prove that the object will never be moved out of the box? – user4815162342 Mar 12 '17 at 20:12
  • @user4815162342 you mean the piece of code from the linked answer that has the warning *"but the created value is highly restricted - **it can never be moved. Notably, this means it cannot be returned from a function**"* (emphasis mine)? The same style of struct that you try to move by returning it from a function? Moving *the box* counts as a move. In this case, the compiler doesn't know anything special about `Box`. This is why owning_ref exists. – Shepmaster Mar 12 '17 at 20:36
  • No need to be snappy, I clearly didn't realize that "moving the box counts as a move" (not part of the answer). So, why does moving the box count as a move? It doesn't move the object, after all. – user4815162342 Mar 12 '17 at 21:18
  • @user4815162342 not trying to be snappy, just pointing out that the exact problem with the exact same context you are talking about is covered in the duplicate, right after the part of code that you adapted for the example. For whatever reason, I have the experience of people not reading the next sentence that already answers the question, so I have taken to just pointing it out again, bolding the relevant aspect, no disrespect intended. – Shepmaster Mar 12 '17 at 21:52
  • @user4815162342 *why does moving the box count as a move* — because it **is a move** and the compiler doesn't know anything special about `Box`. It's the same as *any other struct* where you take a reference to the inside of it. The **compiler doesn't know** that the reference remains valid, even though the programmer does. That's why the linked question has the section *"There is a special case where the lifetime tracking is overzealous: when you have something placed on the heap"* — the compiler doesn't know what `Box` (or `String` or `Vec` or `PathBuf` or ...) does with the internals. – Shepmaster Mar 12 '17 at 21:54
  • No problem, thanks for the clarification. I guess I was thinking of boxing as something that "obviously" guarantees that the object's address won't change, a fact I already [wrote about](http://stackoverflow.com/a/41089221/1600898) in the context of raw pointers. But what applies raw pointers don't track lifetimes, so these problems don't apply to them. – user4815162342 Mar 12 '17 at 22:00
  • 1
    I solved the issue by getting rid of the vector. The code that collected substrings into the vector is now an `Iterator` that produces `&str` items without any allocation. That change made it possible to move the iteration into the closure, which now only needs the string to work. I'm accepting this answer, since it answers the question I actually asked correctly and in quite some detail. – user4815162342 Mar 12 '17 at 22:05