101-move-semantics — Move Semantics
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
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
Copy types (bitwise copy, both bindings valid) from non-Copy types (move, original invalid); OCaml makes no such distinction..clone() explicitly; OCaml structural sharing is implicit.&; 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");
}
}#[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
Rust Approach
Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| See | example.ml | example.rs |
Exercises
String by reference (&String) instead of by value, and confirm the caller retains ownership.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.Wrapper(String) that does not implement Copy. Show that assigning one Wrapper to another moves it, then fix the code using .clone().