Mendelt
Mendelt

Reputation: 37493

Why will a move closure not capture a value with a generic type?

I'm trying to create a ScopeRunner type that can store method calls to a method of types implementing the Scope trait like this:

trait Scope {
    fn run(&self) -> String;
}

struct ScopeImpl;

impl Scope for ScopeImpl {
    fn run(&self) -> String {
        "Some string".to_string()
    }
}


struct ScopeRunner {
    runner: Box<dyn Fn() -> String>,
}

impl ScopeRunner {
    fn new<S: Scope>(scope: S) -> Self {
        ScopeRunner {
            runner: Box::new(move || scope.run())
        }
    }

    pub fn run(self) -> String {
        (self.runner)()
    }

}


fn main() {
    let scope = ScopeImpl {};
    let scope_runner = ScopeRunner::new(scope);

    dbg!(scope_runner.run());
}

I would expect that since ScopeRunner::new creates a move closure this would cause scope to be moved into the closure. But instead the borrow checker gives me this error:

error[E0310]: the parameter type `S` may not live long enough
  --> src/main.rs:21:30
   |
20 |     fn new<S: Scope>(scope: S) -> Self {
   |            -- help: consider adding an explicit lifetime bound `S: 'static`...
21 |         ScopeRunner {runner: Box::new(move || scope.run())}
   |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: ...so that the type `[closure@src/main.rs:21:39: 21:58 scope:S]` will meet its required lifetime bounds
  --> src/main.rs:21:30
   |
21 |         ScopeRunner {runner: Box::new(move || scope.run())}
   | 

When I replace ScopeRunner::new with a non generic version that just takes a ScopeImpl this code does work.

fn new(scope: ScopeImpl) -> Self {
    ScopeRunner {
        runner: Box::new(move || scope.run())
    }
}

I don't understand why this is different. To me it seems the lifetime of the generic Scope would be the same as the concrete version.

Upvotes: 0

Views: 68

Answers (1)

Peter Hall
Peter Hall

Reputation: 58855

The problem is that S could be any type with a Scope impl, which includes all kinds of not-yet-existing types that carry references to other types. For example you could have an implementation like this:

struct AnotherScope<'a> {
    reference: &'str,
}

impl Scope for ScopeImpl {
    fn run(&self) -> String {
        self.reference.to_string()
    }
}

Rust is cautious and wants to make sure that this will work for any qualifying S, including if it contains references.

The easiest fix is to do as the error note suggests and just disallow S from having any non-static references:

fn new<S: Scope + 'static>(scope: S) -> Self {
    ScopeRunner {
        runner: Box::new(move || scope.run())
    }
}

Bounding S with 'static effectively means that S can contain either references to values with a 'static lifetime or no references at all.

If you want to be a bit more flexible, you can broaden that to references that outlive the ScopeRunner itself:

struct ScopeRunner<'s> {
    runner: Box<dyn Fn() -> String + 's>,
}

impl<'s> ScopeRunner<'s> {
    fn new<S: Scope + 's>(scope: S) -> Self {
        ScopeRunner { 
            runner: Box::new(move || scope.run())
        }
    }

    pub fn run(self) -> String {
        (self.runner)()
    }
}

Upvotes: 3

Related Questions