Search code examples
ruststructclosures

Is This Explanation I Was Given (Rust Code Behind The Scenes Requires Consumption of Variable Upon Invocation) Correct?


I was having some trouble understanding closures, so I hopped onto a forum to ask some questions about what went on under the hood. Someone gave me this example:

For the below code:

let x = String::new();
let f = || { println!("{x}") };
f();

The below code is generated by Rust behind the scenes (this is just an approximation, it does not really run):

struct UnnameableClosureType<'a> {
    x0: &'a String,
}

impl<'a> Fn<()> for UnnameableClosureType<'a> {
    type Output = ();

    extern "rust-call" fn call(&self, args: Args) -> Self::Output {
        println!("{}", self.x0);
    }
}

impl<'a> FnMut<()> for UnnameableClosureType<'a> {
    type Output = ();

    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output {
        self.call(args)
    }
}

impl<'a> FnOnce<()> for UnnameableClosureType<'a> {
    type Output = ();

    extern "rust-call" fn call_once(mut self, args: Args) -> Self::Output {
        self.call_mut(args)
    }
}

let x = String::new();
let f = UnnameableClosureType {
    x0: &x
};
f();

Since this requires consumption of x upon invocation, the only trait that can be implemented is FnOnce, and not Fn or FnMut.

That's the explanation I was given.

What I don't really understand is how the compiler knows that FnOnce is the only trait that can be implemented.

I tried suggesting that if the input variable (which was a moved value) got moved to &self.x0 and then dropped, there would be a dangling reference &self.x0, but I'm not really sure if that is correct, and even that sounds a bit weird to me because that describes the outcome that we want to avoid (and even then, I'm not sure if that would actually happen) instead of the rules the compiler uses.

Is the explanation I was given correct, and is my understanding of it sound?


Solution

  • What I don't really understand is how the compiler knows that FnOnce is the only trait that can be implemented.

    That's because it doesn't. The explanation is wrong; the given closure implements all three of them; Fn, FnMut and FnOnce.

    I'm not sure about the exact code the compiler generates, but here's the principle behind it:

    • By default, every closure implements Fn, FnMut and FnOnce, unless something prevents that. Example:
      // Fn + FnMut + FnOnce
      let f = |x| 2*x;
      
    • If a closure performs an action that requires &mut, it will no longer implement Fn, because Fn can only perform immutable actions. Example:
      let mut sum = 0;
      // FnMut + FnOnce
      let f = |x| sum += x;
      
    • If a closure performs an action that requires taking ownership of something, it will no longer implement FnMut or Fn, because both of those do not allow consuming things. Example:
      let s = String::new();
      // Only FnOnce, because `drop` consumes `s`
      let f = || drop(s);
      

    Further, be aware that whether or not a closure is move or not does not influence which of those traits it implements. What does determine these traits is how it uses those captures - for example, if it calls a &mut self function of such a captured object, that would remove the Fn trait, because Fn closures cannote mutate things.