Right, let’s get our hands dirty with the single most important concept you’ll wrestle with in Rust: move semantics. Forget what you know from other languages. In C++, a move is an optimization, a way to say “please don’t copy that giant heap of data, just pilfer its pointers.” In Rust, a move isn’t an optimization; it’s the law. It’s the fundamental mechanism by which ownership—and thus responsibility for cleaning up a value—is transferred from one variable to another.

Here’s the rule, and it’s brutally simple: if a type doesn’t implement the Copy trait (we’ll get to that in a second), assigning it to another variable or passing it as a function argument moves it. The original variable is gone. Kaput. The compiler will treat any further attempt to use it as a personal insult.

fn main() {
    let s1 = String::from("I am the owner"); // s1 owns this String on the heap
    let s2 = s1; // Ownership is MOVED to s2

    // println!("{}", s1); // Uncommenting this will cause a compile-time error:
    // borrow of moved value: `s1`
    println!("{}", s2); // This is perfectly fine. s2 is now the owner.
}

The first time you see this, it feels absurdly restrictive. Why can’t I just have two variables pointing to the same data? The answer is Rust’s core bargain: memory safety without a garbage collector. If both s1 and s2 were valid and both thought they owned that String, we’d have a double-free error waiting to happen when they both went out of scope. Rust avoids this entire class of problems by ensuring there is ever only one owner. The move is the ceremony where the crown of ownership is formally passed to a new monarch.

What’s Actually Happening Under the Hood?

Don’t imagine a String as this big, bloated struct getting physically copied around your program’s memory. That would be painfully slow. A String is essentially three fields: a pointer to the data on the heap, a length, and a capacity. When you move s1 to s2, only those three machine words are copied—a trivial operation. The actual heap-allocated character data just sits there, perfectly content. The magic is that the compiler then invalidates the original variable (s1). It’s a shallow copy followed by the compiler blinding the previous owner. This is why it’s so efficient; it’s all tracked at compile time, with zero runtime cost.

The Copy Trait: The Exception That Proves the Rule

Some types are so simple and cheap to copy that the whole move semantics song and dance is overkill. For these types, Rust has the Copy trait. Types like integers, booleans, and characters are Copy. When you assign them, the compiler actually does a bit-for-bit copy, and the original variable remains perfectly valid.

fn main() {
    let x = 42; // i32 is Copy
    let y = x;  // This is a COPY, not a move.

    println!("x is {}, y is {}", x, y); // Both are perfectly fine.
}

The key insight is that Copy and move semantics are mutually exclusive. A type cannot be Copy if it implements Drop (the trait for custom cleanup logic), because the whole point of Copy is that no special cleanup is needed. The compiler needs to know it can just duplicate bits and forget about the original. A String is not Copy because copying its pointer, length, and capacity would create two owners for the same heap data, violating Rust’s core safety guarantee.

Function Calls: The Great Ownership Sink

This is where moves really trip people up. Passing a non-Copy value to a function is a move. The function now becomes the owner.

fn take_ownership(t: String) { // t comes into scope, owns the String
    println!("I now own: {}", t);
} // t goes out of scope, `drop` is called, memory is freed.

fn main() {
    let my_string = String::from("hello");
    take_ownership(my_string); // MOVES ownership into the function

    // println!("{}", my_string); // Error! Value was moved.
}

This feels harsh, but it’s brilliantly consistent. The function take_ownership is now responsible for the string, and the caller has lost all rights to it. This pattern forces you to think very deliberately about who needs what and for how long. If you want a function to use a value without taking ownership, you need to lend it to the function. That’s called borrowing, and it’s the very next thing we’re going to talk about. Consider this the natural conclusion of our discussion on moves: they create the problem that borrowing elegantly solves.