ExamplesBy LevelBy TopicLearning Paths
101 Intermediate

101-move-semantics — Move Semantics

Functional Programming

Tutorial

The Problem

Memory safety without a garbage collector requires a clear ownership model. C++ introduced move semantics in C++11 to avoid expensive deep copies when transferring ownership of heap resources. Rust takes this further: every value has exactly one owner, and assigning a non-Copy value to a new variable transfers ownership — the original binding becomes invalid. The compiler enforces this statically, eliminating use-after-free and double-free at zero runtime cost.

OCaml sidesteps the problem with a garbage collector that tracks all references. Rust's move semantics achieve the same memory safety guarantee at compile time, with no runtime overhead.

🎯 Learning Outcomes

  • • Understand Rust's ownership rule: each value has exactly one owner
  • • Distinguish between types that move (heap-allocated) and types that copy (stack-only)
  • • Know that passing a value to a function transfers ownership unless the function borrows
  • • Understand how returning a value transfers ownership back to the caller
  • • Recognise the compiler error that results from using a moved value
  • Code Example

    #![allow(clippy::all)]
    // 101: Move Semantics
    // Ownership transfer — after move, original is invalid
    
    // Approach 1: Move with String (heap-allocated)
    fn take_ownership(s: String) -> usize {
        s.len() // s is consumed here
    }
    
    fn demonstrate_move() {
        let s = String::from("hello");
        let len = take_ownership(s);
        // println!("{}", s); // ERROR: s has been moved!
        assert_eq!(len, 5);
    }
    
    // Approach 2: Copy types don't move
    fn demonstrate_copy() {
        let x = 42;
        let y = x; // copy, not move — x is still valid
        assert_eq!(x, 42);
        assert_eq!(y, 42);
    }
    
    // Approach 3: Move in collections
    fn demonstrate_vec_move() {
        let v1 = vec![1, 2, 3];
        let v2 = v1; // v1 is moved to v2
                     // println!("{:?}", v1); // ERROR: v1 has been moved
        assert_eq!(v2, vec![1, 2, 3]);
    }
    
    // Return value transfers ownership back
    fn create_string() -> String {
        let s = String::from("created");
        s // ownership transferred to caller
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_take_ownership() {
            let s = String::from("hello");
            assert_eq!(take_ownership(s), 5);
        }
    
        #[test]
        fn test_copy_types() {
            let x = 42;
            let y = x;
            assert_eq!(x + y, 84);
        }
    
        #[test]
        fn test_vec_move() {
            let v1 = vec![1, 2, 3];
            let v2 = v1;
            assert_eq!(v2.len(), 3);
        }
    
        #[test]
        fn test_return_ownership() {
            let s = create_string();
            assert_eq!(s, "created");
        }
    }

    Key Differences

  • Enforcement: Rust's move semantics are enforced at compile time; OCaml's GC handles safety at runtime with no such restriction.
  • Copy types: Rust distinguishes Copy types (bitwise copy, both bindings valid) from non-Copy types (move, original invalid); OCaml makes no such distinction.
  • Explicit clone: To keep ownership in Rust after a move, call .clone() explicitly; OCaml structural sharing is implicit.
  • Function call semantics: In Rust, passing a value to a function moves it unless you borrow with &; in OCaml, all arguments are passed by value but the GC tracks the underlying data.
  • OCaml Approach

    OCaml has no move semantics. All values are managed by the GC, and bindings are references into heap-allocated nodes. You can freely pass a string to multiple functions — the GC ensures the value lives as long as any binding refers to it. The simulated ownership model in example.ml uses ref and a custom Moved sentinel to illustrate the concept, but the compiler does not enforce it.

    Full Source

    #![allow(clippy::all)]
    // 101: Move Semantics
    // Ownership transfer — after move, original is invalid
    
    // Approach 1: Move with String (heap-allocated)
    fn take_ownership(s: String) -> usize {
        s.len() // s is consumed here
    }
    
    fn demonstrate_move() {
        let s = String::from("hello");
        let len = take_ownership(s);
        // println!("{}", s); // ERROR: s has been moved!
        assert_eq!(len, 5);
    }
    
    // Approach 2: Copy types don't move
    fn demonstrate_copy() {
        let x = 42;
        let y = x; // copy, not move — x is still valid
        assert_eq!(x, 42);
        assert_eq!(y, 42);
    }
    
    // Approach 3: Move in collections
    fn demonstrate_vec_move() {
        let v1 = vec![1, 2, 3];
        let v2 = v1; // v1 is moved to v2
                     // println!("{:?}", v1); // ERROR: v1 has been moved
        assert_eq!(v2, vec![1, 2, 3]);
    }
    
    // Return value transfers ownership back
    fn create_string() -> String {
        let s = String::from("created");
        s // ownership transferred to caller
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_take_ownership() {
            let s = String::from("hello");
            assert_eq!(take_ownership(s), 5);
        }
    
        #[test]
        fn test_copy_types() {
            let x = 42;
            let y = x;
            assert_eq!(x + y, 84);
        }
    
        #[test]
        fn test_vec_move() {
            let v1 = vec![1, 2, 3];
            let v2 = v1;
            assert_eq!(v2.len(), 3);
        }
    
        #[test]
        fn test_return_ownership() {
            let s = create_string();
            assert_eq!(s, "created");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_take_ownership() {
            let s = String::from("hello");
            assert_eq!(take_ownership(s), 5);
        }
    
        #[test]
        fn test_copy_types() {
            let x = 42;
            let y = x;
            assert_eq!(x + y, 84);
        }
    
        #[test]
        fn test_vec_move() {
            let v1 = vec![1, 2, 3];
            let v2 = v1;
            assert_eq!(v2.len(), 3);
        }
    
        #[test]
        fn test_return_ownership() {
            let s = create_string();
            assert_eq!(s, "created");
        }
    }

    Deep Comparison

    Core Insight

    OCaml's GC handles memory; Rust moves ownership — after a move, the original binding is invalid

    OCaml Approach

  • • See example.ml for implementation
  • Rust Approach

  • • See example.rs for implementation
  • Comparison Table

    FeatureOCamlRust
    Seeexample.mlexample.rs

    Exercises

  • Write a function that accepts a String by reference (&String) instead of by value, and confirm the caller retains ownership.
  • Implement a clone_and_modify function that takes a Vec<i32>, clones it, appends a value to the clone, and returns both the original and the modified copy.
  • Create a struct Wrapper(String) that does not implement Copy. Show that assigning one Wrapper to another moves it, then fix the code using .clone().
  • Open Source Repos