Can Rust understand that code will be executed only once and not report a related error?

I get the following error: "value moved here, in previous iteration of loop". I just want to understand, is it possible to prove that the attempt to move the value will happen only once? i is immutable and therefore cannot be changed, and the range goes through 0, so would it be okay to create a github issue asking for an enhancement?

struct S {
    field: i32,
}

fn main() {
    let s = S {field: 0};
    let mut v: Vec<S> = vec![];
    for i in 0..10 {
        if i == 0 {
            v.push(s);
        }
        /* this does not work either */
        match i {
            0 => { v.push(s); },
            _ => (),
        }
    }
}

No, ownership is based on scopes, and can't depend on values. The analysis is shallow and works based on broad rules what code may do, not on detailed analysis of what it actually does.

You'll need to wrap it in Option, and use s.take().unwrap(). The optimizer may later do careful loop analysis to optimize that out.

3 Likes

As far as I can tell it may be obvious in this very simple case that a move can only happen once. But in general it is impossible.

For example what if your test were "if i == someMysteriousFunction() {..."

That snippet makes little sense anyway, so perhaps the error is a good indication that it should be changed.

I mean, as you now the loop passes through zero and you know the push happens at zero. Then why not just put the push outside the loop?

1 Like

Well, this code is a bit artificial so I’m not 100% how of any actual code that would “need” this kind of analysis would look like. Furthermore the reason why i == 0 can only happen once is a bit less trivial than one might think; ultimately iteration through ranges like 0..10 is implemented in the standard library, and not some kind of compiler magic.

In this particular case also, since its the first iteration, it seems so trivial just to move this first iteration out of the loop.

In the general case there’s always the possibility to use a dynamic check that would panic if you try to take the value twice. I mean something like this:

struct S {
    field: i32,
}

fn main() {
    let mut s = Some(S {field: 0});
    let mut v: Vec<S> = vec![];
    for i in 0..10 {
        if i == 0 {
            v.push(s.take().unwrap());
        }
    }
}
2 Likes

Yes, this example is artificial, but that Some trick is something I did not think about. Interesting.

To answer a related question, note that there are versions of this where the control flow makes it clear that the move will only happen once, which rust does allow:

struct S {
    field: i32,
}

fn main() {
    let s = S {field: 0};
    let mut v: Vec<S> = vec![];
    for i in 0..10 {
        if i == 0 {
            v.push(s);
            break; // NEW!
        }
    }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c09d4e24f11c7178eedb89c4c349d1d6

I don't know whether that can work in your actual case, but maybe.

2 Likes

As far as I understand the borrow checker only looks at one function at a time. It does not attempt to follow things outside of a function.

So presumably one could find all kind of cases where one can tell pretty easily that a move will only happen once, or whatever, and wonder why the borrow checker does not see that as well.

Arguably enough code analysis could be thrown at the problem to find many of those cases.

Or one could argue: If it is so obvious to you that the situation cannot arise then why have you written your code that looks like it might arise? Would that not be an obfuscation that is better simplified?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.