895-move-semantics — Move Semantics
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
&T) to share without transferring ownershipmove keyword)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
move for ownership, implicit reference otherwise); OCaml closures capture by reference implicitly.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);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| String type | string (immutable, GC-managed) | String (owned, heap) / &str (borrowed slice) |
| Passing a string | val f : string -> int — always shared | fn f(s: String) moves; fn f(s: &str) borrows |
| Ownership | implicit — GC tracks all refs | explicit — one owner, tracked statically |
| Memory reclaim | GC pause, non-deterministic | deterministic drop at end of owner scope |
| Copy semantics | all values implicitly shareable | only Copy types copy; others move |
Key Insights
free calls automatically — at exactly the right point, with zero runtime overhead.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.&str or &T — this lets callers keep ownership while the function borrows temporarily.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
take_and_return(s: String) -> (String, usize) that returns both the string and its length, demonstrating how to give ownership back.Builder struct that takes a Vec<String> by move, appends items mutably, and returns the completed Vec<String> when .build() is called.HashMap<String, i32> by move and returns a lookup function — explain why move is necessary.