ExamplesBy LevelBy TopicLearning Paths
895 Intermediate

895-move-semantics — Move Semantics

Functional Programming

Tutorial

The Problem

In C++, copying data is implicit and expensive — passing a std::string to a function copies the heap allocation by default. C++11 introduced move semantics as an opt-in optimization. Rust inverts the default: passing a value to a function always moves ownership, making the original binding invalid. This prevents use-after-free at compile time without a garbage collector. OCaml avoids the issue through garbage collection — all values are GC-managed, and the runtime ensures safety. Understanding Rust's move semantics is the entry point to its ownership model and the foundation for the borrow checker.

🎯 Learning Outcomes

  • • Understand that passing a heap-owning type by value transfers ownership in Rust
  • • Recognize the compiler error when attempting to use a moved value
  • • Use borrowing (&T) to share without transferring ownership
  • • Implement closures that capture by move (move keyword)
  • • Compare with OCaml's GC-managed model where ownership is not a programmer concern
  • Code Example

    pub fn borrow_string(s: &str) -> usize {
        s.len()
    }
    
    fn main() {
        let greeting = String::from("Hello, ownership!");
        let len1 = borrow_string(&greeting);
        let len2 = borrow_string(&greeting); // fine — borrowed, not moved
        assert_eq!(len1, len2);
    }

    Key Differences

  • Default behavior: Rust moves ownership by default for heap types; OCaml passes a pointer with GC tracking — no ownership transfer.
  • Compile-time vs runtime safety: Rust proves memory safety at compile time via the borrow checker; OCaml proves it at runtime via the GC.
  • Closure capture: Rust closures must declare capture mode (move for ownership, implicit reference otherwise); OCaml closures capture by reference implicitly.
  • Copy types: Rust Copy types (integers, booleans, etc.) are copied bitwise on assignment — no move. OCaml integers are always passed by value too.
  • OCaml Approach

    OCaml has no move semantics. All values are heap-allocated and GC-managed (except small integers and unboxed floats in certain contexts). Passing a value to a function passes a pointer — the original binding remains valid. "Ownership" is not a concept in OCaml; instead, the GC tracks reachability. Closures capture values by reference implicitly. The trade-off: OCaml avoids ownership complexity at the cost of GC pauses and less predictable memory behavior.

    Full Source

    #![allow(clippy::all)]
    // Example 895: Move Semantics — Rust Ownership Transfer
    //
    // In Rust, values have a single owner. When you pass a value to a function,
    // ownership transfers (moves) and the original binding becomes invalid.
    // This is how Rust prevents use-after-free at compile time — no GC needed.
    
    // Approach 1: Move with String (heap-allocated, non-Copy)
    // Takes ownership of s — caller cannot use s after this call
    pub fn consume_string(s: String) -> usize {
        s.len()
    }
    
    // Approach 2: Borrow instead of move — caller retains ownership
    pub fn borrow_string(s: &str) -> usize {
        s.len()
    }
    
    // Approach 3: Move with a struct (non-Copy by default)
    #[derive(Debug, PartialEq)]
    pub struct Person {
        pub name: String,
        pub age: u32,
    }
    
    // Consumes person — ownership transfers into the function
    pub fn greet(p: Person) -> String {
        format!("Hello, {} (age {})!", p.name, p.age)
    }
    
    // Borrows person — caller retains ownership
    pub fn greet_ref(p: &Person) -> String {
        format!("Hello, {} (age {})!", p.name, p.age)
    }
    
    // Approach 4: Returning ownership — transfer back to caller
    pub fn make_greeting(name: &str) -> String {
        format!("Hello, {}!", name)
    }
    
    // Approach 5: Move in a closure context
    // The closure captures `prefix` by move (it's a String)
    pub fn make_prefixer(prefix: String) -> impl Fn(&str) -> String {
        move |s| format!("{}: {}", prefix, s)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_consume_string_transfers_ownership() {
            let greeting = String::from("Hello, ownership!");
            let len = consume_string(greeting);
            // `greeting` is moved — we cannot use it here anymore.
            // The compiler would reject: consume_string(greeting) a second time.
            assert_eq!(len, 17);
        }
    
        #[test]
        fn test_borrow_does_not_move() {
            let greeting = String::from("Hello, ownership!");
            // Borrow once
            let len1 = borrow_string(&greeting);
            // greeting still valid — borrow returned ownership implicitly
            let len2 = borrow_string(&greeting);
            assert_eq!(len1, len2);
            assert_eq!(len1, 17);
            // greeting is still usable here
            assert_eq!(greeting, "Hello, ownership!");
        }
    
        #[test]
        fn test_struct_move_consumes_value() {
            let alice = Person {
                name: String::from("Alice"),
                age: 30,
            };
            let msg = greet(alice);
            // `alice` is moved into greet — no longer accessible here
            assert_eq!(msg, "Hello, Alice (age 30)!");
        }
    
        #[test]
        fn test_struct_borrow_retains_ownership() {
            let bob = Person {
                name: String::from("Bob"),
                age: 25,
            };
            let msg1 = greet_ref(&bob);
            let msg2 = greet_ref(&bob); // bob still alive
            assert_eq!(msg1, msg2);
            assert_eq!(bob.name, "Bob"); // bob is still usable
        }
    
        #[test]
        fn test_return_transfers_ownership_to_caller() {
            let msg = make_greeting("World");
            // Caller owns `msg` now
            assert_eq!(msg, "Hello, World!");
        }
    
        #[test]
        fn test_closure_captures_by_move() {
            let prefix = String::from("LOG");
            let prefixer = make_prefixer(prefix);
            // `prefix` is moved into the closure — no longer usable here
            assert_eq!(prefixer("info message"), "LOG: info message");
            assert_eq!(prefixer("error message"), "LOG: error message");
        }
    
        #[test]
        fn test_copy_types_do_not_move() {
            // i32 implements Copy — assignment copies, not moves
            let x: i32 = 42;
            let y = x; // copy, not move
            assert_eq!(x, 42); // x still valid
            assert_eq!(y, 42);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_consume_string_transfers_ownership() {
            let greeting = String::from("Hello, ownership!");
            let len = consume_string(greeting);
            // `greeting` is moved — we cannot use it here anymore.
            // The compiler would reject: consume_string(greeting) a second time.
            assert_eq!(len, 17);
        }
    
        #[test]
        fn test_borrow_does_not_move() {
            let greeting = String::from("Hello, ownership!");
            // Borrow once
            let len1 = borrow_string(&greeting);
            // greeting still valid — borrow returned ownership implicitly
            let len2 = borrow_string(&greeting);
            assert_eq!(len1, len2);
            assert_eq!(len1, 17);
            // greeting is still usable here
            assert_eq!(greeting, "Hello, ownership!");
        }
    
        #[test]
        fn test_struct_move_consumes_value() {
            let alice = Person {
                name: String::from("Alice"),
                age: 30,
            };
            let msg = greet(alice);
            // `alice` is moved into greet — no longer accessible here
            assert_eq!(msg, "Hello, Alice (age 30)!");
        }
    
        #[test]
        fn test_struct_borrow_retains_ownership() {
            let bob = Person {
                name: String::from("Bob"),
                age: 25,
            };
            let msg1 = greet_ref(&bob);
            let msg2 = greet_ref(&bob); // bob still alive
            assert_eq!(msg1, msg2);
            assert_eq!(bob.name, "Bob"); // bob is still usable
        }
    
        #[test]
        fn test_return_transfers_ownership_to_caller() {
            let msg = make_greeting("World");
            // Caller owns `msg` now
            assert_eq!(msg, "Hello, World!");
        }
    
        #[test]
        fn test_closure_captures_by_move() {
            let prefix = String::from("LOG");
            let prefixer = make_prefixer(prefix);
            // `prefix` is moved into the closure — no longer usable here
            assert_eq!(prefixer("info message"), "LOG: info message");
            assert_eq!(prefixer("error message"), "LOG: error message");
        }
    
        #[test]
        fn test_copy_types_do_not_move() {
            // i32 implements Copy — assignment copies, not moves
            let x: i32 = 42;
            let y = x; // copy, not move
            assert_eq!(x, 42); // x still valid
            assert_eq!(y, 42);
        }
    }

    Deep Comparison

    OCaml vs Rust: Move Semantics

    Side-by-Side Code

    OCaml

    (* OCaml: GC manages memory — values are shared freely *)
    let use_string s =
      String.length s
    
    let () =
      let greeting = "Hello, ownership!" in
      let len1 = use_string greeting in
      let len2 = use_string greeting in  (* perfectly fine — no move *)
      assert (len1 = len2)
    

    Rust (idiomatic — borrow to avoid move)

    pub fn borrow_string(s: &str) -> usize {
        s.len()
    }
    
    fn main() {
        let greeting = String::from("Hello, ownership!");
        let len1 = borrow_string(&greeting);
        let len2 = borrow_string(&greeting); // fine — borrowed, not moved
        assert_eq!(len1, len2);
    }
    

    Rust (ownership transfer — move semantics)

    pub fn consume_string(s: String) -> usize {
        s.len()
        // s is dropped here — memory freed immediately
    }
    
    fn main() {
        let greeting = String::from("Hello, ownership!");
        let len = consume_string(greeting);
        // greeting is GONE — compiler rejects any further use
        assert_eq!(len, 17);
    }
    

    Type Signatures

    ConceptOCamlRust
    String typestring (immutable, GC-managed)String (owned, heap) / &str (borrowed slice)
    Passing a stringval f : string -> int — always sharedfn f(s: String) moves; fn f(s: &str) borrows
    Ownershipimplicit — GC tracks all refsexplicit — one owner, tracked statically
    Memory reclaimGC pause, non-deterministicdeterministic drop at end of owner scope
    Copy semanticsall values implicitly shareableonly Copy types copy; others move

    Key Insights

  • No GC required: Rust's ownership model gives the compiler enough information to insert free calls automatically — at exactly the right point, with zero runtime overhead.
  • Move = compile-time transfer: When you pass a String to a function in Rust, the compiler treats the original binding as dead. Any subsequent use is a compile error, not a runtime crash.
  • Borrow is the idiomatic escape hatch: Most functions that only read data should take &str or &T — this lets callers keep ownership while the function borrows temporarily.
  • OCaml's GC is the difference: In OCaml every value is heap-allocated and reference-counted/traced. You can alias freely because the GC ensures the memory lives as long as any reference exists. Rust instead enforces a single owner so it can use stack discipline for memory management.
  • Copy types opt out of move: Primitives like i32, bool, char implement Copy — assignment duplicates them bitwise, so the original remains valid. String and structs containing heap data do not implement Copy by default.
  • When to Use Each Style

    **Use move (owned String / T):** When the function needs to store, transform, or return the value — it takes full responsibility for the data's lifetime.

    **Use borrow (&str / &T):** When the function only reads or inspects the value and the caller should retain ownership after the call. This is the default choice for most function parameters.

    Exercises

  • Write a function take_and_return(s: String) -> (String, usize) that returns both the string and its length, demonstrating how to give ownership back.
  • Implement a Builder struct that takes a Vec<String> by move, appends items mutably, and returns the completed Vec<String> when .build() is called.
  • Write a closure that captures a HashMap<String, i32> by move and returns a lookup function — explain why move is necessary.
  • Open Source Repos