How does ownership work with structs

Struct ownership transfers all field ownership when the struct is moved, invalidating the original variable.

The backpack moves with the gear

You're building a text adventure game. You define a Player struct with a name, inventory, and health. You write a function save_game that takes a Player. You call save_game(player). The function runs. Then you try to print the player's name to the console. The compiler throws an error. E0382. You didn't pass the player; you handed the player over. The variable is gone.

This is struct ownership in action. A struct is a container for related data. Ownership flows through the container. If a field owns its data, the struct owns that data too. Moving the struct moves every field inside it. You cannot keep the struct and also keep the data it contains.

Think of a struct like a backpack. The struct is the bag. The fields are the gear strapped inside. When you hand the backpack to a friend, you hand over the bag and everything in it. You can't keep the backpack and also keep the tent inside it. The tent moved with the bag. Rust enforces this rule to prevent double frees and dangling pointers. If the struct moves, all the memory it manages moves with it.

Minimal example

Here is a User struct with a String and a bool. The String owns heap memory. The bool lives on the stack. When you assign the struct to a new variable, the entire struct moves.

struct User {
    username: String, // String owns heap data.
    active: bool,     // bool is Copy, lives on stack.
}

fn main() {
    // Create user1. It owns the heap string and the bool.
    let user1 = User {
        username: String::from("alice"),
        active: true,
    };

    // Assign user1 to user2.
    // This moves the entire struct.
    // user1 loses ownership of everything inside.
    let user2 = user1;

    // user2 now owns the string and the bool.
    println!("{}", user2.username);

    // user1 is invalid.
    // println!("{}", user1.username); // Error E0382: use of moved value
}

The compiler rejects access to user1 after the move with E0382 (use of moved value). The move happens because User contains a String. String does not implement the Copy trait. When a struct contains a non-Copy field, the struct itself does not implement Copy. The assignment let user2 = user1 transfers ownership. user1 is marked as uninitialized. user2 becomes the sole owner.

What happens in memory

When you create user1, the compiler allocates space on the stack for the struct. The struct contains a pointer to the heap string and the boolean value. The heap string holds the actual characters.

When you execute let user2 = user1, the compiler generates code that copies the pointer and the boolean from user1 to user2. The heap string is not copied. The pointer is copied. After the copy, the compiler invalidates user1. It treats user1 as if it never existed. If you tried to drop user1 later, the compiler would prevent it. This ensures the heap string is dropped exactly once, when user2 goes out of scope.

The boolean is copied bitwise, but that copy is part of the struct move. The struct move is the dominant action. The struct moves as a single unit. You cannot move the boolean and leave the string behind.

The Copy trait changes everything

If every field in a struct implements Copy, the struct can also implement Copy. A Copy type is duplicated bitwise. No ownership transfer occurs. The original value remains valid.

You must opt into Copy explicitly. The compiler does not assume a struct is Copy even if all fields are Copy. Add #[derive(Copy, Clone)] to the struct definition.

#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 10, y: 20 };

    // p1 is copied to p2.
    // p1 remains valid.
    let p2 = p1;

    println!("p1: {}, {}", p1.x, p1.y); // Works
    println!("p2: {}, {}", p2.x, p2.y); // Works
}

p1 and p2 are independent copies. Changing p2 does not affect p1. The compiler generates code that copies the bits. No heap allocation is involved. No ownership transfer occurs.

Convention aside: always derive Clone when you derive Copy. Copy is a subtrait of Clone. A type cannot be Copy without being Clone. The compiler requires both. Writing #[derive(Copy, Clone)] is the standard pattern. It signals to readers that the type is cheap to duplicate.

Destructuring and partial moves

You cannot move a single field out of a struct and leave the rest behind. Rust prevents partial moves on a struct. If you try to move a field, the compiler consumes the entire struct.

struct User {
    username: String,
    active: bool,
}

fn main() {
    let user = User {
        username: String::from("alice"),
        active: true,
    };

    // Destructure to move username out.
    // This consumes user.
    let User { username, .. } = user;

    println!("{}", username); // Works

    // user is gone.
    // println!("{}", user.active); // Error E0382
}

The pattern User { username, .. } moves username out of user. The .. discards the remaining fields. The entire user variable is consumed. You cannot access user after the destructuring. The compiler treats the destructuring as a move of the struct, followed by a move of the specific field.

If you need to keep the struct and extract a field, you must clone the field or borrow it. Cloning creates a new value. Borrowing creates a reference. Neither moves the original data.

Borrowing structs to keep ownership

When you need to use a struct without taking ownership, pass a reference. A reference borrows the data. The struct stays with the owner. The reference is a pointer that the compiler tracks.

struct User {
    username: String,
    active: bool,
}

/// Prints user info by borrowing.
fn print_user(user: &User) {
    // user is a reference.
    // Ownership stays with the caller.
    println!("User: {}", user.username);
}

fn main() {
    let user = User {
        username: String::from("alice"),
        active: true,
    };

    // Pass a reference.
    print_user(&user);

    // user is still valid.
    println!("Still here: {}", user.username);
}

&user creates an immutable reference. The reference is copied freely. print_user borrows the struct. It cannot move the struct. It cannot drop the struct. When print_user returns, the borrow ends. The owner retains full control.

Use &mut User when you need to modify the struct. Mutable borrowing follows the same rules. The struct stays with the owner. The reference allows mutation. The compiler ensures no other borrows exist while the mutable borrow is active.

Pitfalls and compiler errors

A common mistake is assuming a struct is Copy because it contains a bool or i32. The struct is only Copy if every field is Copy. If one field is String, Vec, or Box, the struct moves. The compiler rejects Copy for the struct. You get E0382 when you try to use the struct after a move.

Another pitfall is forgetting #[derive(Copy, Clone)]. If your struct only contains Copy types, you can derive Copy. Without the derive, the struct moves. The compiler does not infer Copy. You must add the attribute.

If a struct implements Drop, it cannot be Copy. Drop defines custom cleanup logic. The cleanup runs when the owner goes out of scope. If the type were Copy, multiple owners could exist. The compiler would not know which copy should run Drop. This ambiguity is forbidden. Drop and Copy are mutually exclusive.

The compiler error E0382 is your friend. It catches use-after-move bugs at compile time. In C or C++, moving a struct might leave a dangling pointer. Rust prevents this. If the compiler says moved, it is moved. Trust the error. Fix the code by borrowing or cloning.

Decision matrix

Use struct ownership when you have a group of related values that should live and die together. The struct moves as one unit, keeping the data coherent. Use references when you need to inspect or modify a struct without taking ownership. Pass &User or &mut User to functions that borrow the data. Use Rc<T> or Arc<T> when multiple parts of your program need to own the same struct. Reference counting allows shared ownership. Use Box<T> when the struct is very large and you want to move a pointer instead of copying the entire struct on the stack. Boxing shifts the data to the heap. Use #[derive(Copy, Clone)] when every field is cheap to copy and you want bitwise duplication. The struct copies instead of moving.

The struct is the backpack. You can't keep the bag and the tent. Derive Copy if you can. Otherwise, the struct moves. Borrow when you don't need to own. Trust the compiler. If it says moved, it's moved.

Where to go next