Use of moved value OK if we return

OK, didn't know how to title it but in a nutshell:

let mut handles:Vec<JoinHandle<()>> = Vec::new();
while let Some(v) =  rx.recv().await
        {
           
            if v.len() == 0
            {
                for handle in handles
                {
                    handle.await;
                }
                return Ok(());//comment that out and you will get error talking about using moved value, i.e. handles
            }
            handles.push(tokio::spawn(async move {

            }));
        }

I am really, really confused by this. Why without return this code is invalid and with return this code is valid?

This uses IntoIterator on handles, which consumes it.

This means that without the return, you try to use a moved value on the handles.push(...) line.

Consider using .iter() or .iter_mut() on handles in the for loop to avoid moving it.

Hi, my point is why having return there makes the program valid.
Fox in mind, that return returns not just from that if statement.

Why wouldn’t it be valid? The compiler is smart enough to figure out that if there’s an unconditional return (or any divergence, break would work here too), handles is only ever consumed once, and the code is fine. Whereas without the return it must assume that the if body is reached twice and tries to access handles after it was moved from in an earlier iteration, which it cannot allow.

1 Like

But there isn't unconditional return; The return is only if the lenght is zero.

If the length isn't zero, the for loop doesn't happen so handles is not consumed, so there is no problem.

1 Like

What about len > 10. I'm trying to understand the reasoning here.

The compiler does not reason about conditions like v.len() > 0 or v.len() > 10 etc etc, it just looks at what code paths are possible. In particular here the execution flow has 2 possibilities: enter the if or not. The compiler doesn't know which one will actually happen, so it has to check both of them to ensure that everything is fine is both cases:

  • If it doesn't then handles remains valid and everything is good.
  • If however it enters the if then handles is consumed.
    • In the code without the return this is problematic because from there the code flow continues until the handles.push(...), which requires handles to be valid, hence the error
    • In the code with the return, this is not an issue, because there's no way for the execution to reach the handles.push(...) after consuming handles in the for loop, since it will always hit the return and exit from the function before that can happen.

In the version without the return there's also another issue that can be seen by commenting out the handles.push(...): after an iteration is executed the outer while let will run again, and this may reach the for handle in handles again, which is problematic because handles was consumed in the prior iteration. The way the compiler sees this is by rerunning the analysis on the loop body after finding out which variables are consumed in the first analysis run. More in general this falls under the umbrella of dataflow analysis.

3 Likes

Thanks, that makes sense.