Search code examples
if-statementscoperustborrowing

"if" statement gets executed when the condition is "false" in a Rust program, how to understand it?


For the following Rust program:

fn main() {
    let foo = "test".to_string();
    if false {
        let _bar = foo; // value moved to _bar
    }
    println!("{}", foo);
}

I got this error when run it:

error[E0382]: borrow of moved value: `foo`
 --> src\main.rs:6:20
  |
2 |     let foo = "test".to_string();
  |         --- move occurs because `foo` has type `std::string::String`, which does not implement the `Copy` trait
3 |     if false {
4 |         let _bar = foo; // value moved to _bar
  |                    --- value moved here
5 |     }
6 |     println!("{}", foo);
  |                    ^^^ value borrowed here after move

Could anyone help to explain what happens here? It's weird to me that the move happens in a if statement which will never be true. Also I want to know more about this situation, what keywords should I use to search?


Solution

  • Here's the secret to moves: they don't really exist.

    Moves generate no code (in the sense of machine code) that is different from a bitwise copy.¹ The only difference between a move and a copy is what happens to the "original": if it's still valid, it's a copy; if the original is no longer valid, it's a move.

    So how does the compiler enforce that you don't use the original value after a move? There's no runtime flag that keeps track of whether foo is valid or not.² Instead, the compiler uses source code analysis to determine, at compile time, whether foo is definitely valid or may have been moved out of when you try to use it. Because this analysis takes place at compile time, it doesn't follow the flow of execution within the function; it happens for the whole function at once. The compiler sees that foo is moved out of inside the if, and rejects the later use of foo without evaluating the condition or any code.

    A smart compiler could take control flow into account when doing validity analysis,³ but that might not be an improvement. It's not always possible to know whether a branch is taken (it's undecidable), so there would be cases where the compiler would still get it wrong. Also, as Cerberus noted in the question comments, it would greatly slow down that compiler pass.

    Put another way: In Rust, you never explicitly move something. You just do whatever you want with it, and let the compiler tell you whether you did it wrong or not, according to whether the type is Copy and whether it's used later. This is unlike C++, where moving is an operation that may call a "move constructor" and have side effects; in Rust, it's a purely static, pass/fail check. If you did it right, the program passes and moves on to the next stage of compilation; if you did it wrong, the borrow checker will tell you (and hopefully help you fix it).

    See also


    ¹ Unless the moved type implements Drop, in which case the compiler may emit drop flags.

    ² Actually, there is (the drop flag), but it's only checked when foo is dropped, not at each use. Types that don't implement Drop don't have drop flags, even though they have the same move semantics.

    ³ This is similar to how null checking works in Kotlin: if the compiler can figure out that a reference is definitely non-null, it will allow you to dereference it. Validity analysis in Rust is more conservative than that; the compiler doesn't even try to guess.