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.