Files
rust-patterns/patterns/ownership.md
T
Rodin 6661a9f249 docs: ownership and lifetime patterns from rust-lang/rust
10 patterns, 677 lines. Full spec compliance.
Patterns: borrowing over owning, Clone/Copy, Cow, mem::take, Box,
Arc, Drop/RAII, lifetime elision, AsRef, PhantomData.
2026-04-30 14:57:23 -07:00

18 KiB

Rust Ownership & Lifetime Patterns

Patterns for ownership, borrowing, and lifetimes in Rust, extracted from the standard library source.

Source: rust-lang/rust at commit f53b654

Stats: 7,909 &mut references, 798 Rc/Arc, 697 Box<>, 410 Cell/RefCell, 197 Cow<>, 254 Drop impls, 401 PhantomData, 112 mem::take/replace.


1. Borrowing Over Owning in Parameters

Source:

library/alloc/src/string.rs (String/str relationship)

// The stdlib accepts &str not String for read-only access:
impl str {
    pub fn contains<P: Pattern>(&self, pat: P) -> bool { ... }
    pub fn starts_with<P: Pattern>(&self, pat: P) -> bool { ... }
}

Why

Taking &T instead of T means the caller keeps ownership. This is the most fundamental Rust pattern: borrow when you only need to read, own when you need to keep or modify.

When to Use

Triggers:

  • You only need to read the value (don't need to store it)
  • You want to accept both owned and borrowed values (String and &str)
  • You don't want to force callers to clone

Example — before:

// Takes ownership unnecessarily — forces caller to clone
fn greet(name: String) {
    println!("Hello, {name}!");
}

let name = String::from("Alice");
greet(name);  // name is moved — can't use it anymore

Example — after:

// Borrows — caller keeps ownership
fn greet(name: &str) {
    println!("Hello, {name}!");
}

let name = String::from("Alice");
greet(&name);   // works with &String (deref coercion)
greet("Bob");   // works with &str directly
// name is still usable here

When NOT to Use

Don't use this when:

  • You need to store the value in a struct (take ownership)
  • You need to send it to another thread (ownership needed for Send)
  • The function will outlive the caller (return a Future that needs 'static)

2. Clone vs Copy Semantics

Source:

library/core/src/clone.rs, library/core/src/marker.rs (Copy)

Top derives: Clone (880), Copy (537).

// Copy = implicit bitwise copy (stack only, no heap)
#[derive(Clone, Copy)]
pub struct Duration { secs: u64, nanos: Nanoseconds }

// Clone only = explicit .clone() needed (may allocate)
#[derive(Clone)]
pub struct String { vec: Vec<u8> }  // Can't be Copy — heap allocated

Why

Copy types are implicitly duplicated on assignment (like integers). Clone types require explicit .clone(). The distinction prevents accidental expensive copies. If you can be Copy, you should be.

When to Use

Triggers:

  • Copy: Type is small, stack-only, bitwise-copyable (no heap, no Drop impl, no &mut interior)
  • Clone only: Type allocates, has Drop, or copying is expensive

Example — before:

// Missing Copy on a small stack type
struct Point { x: f64, y: f64 }

let p = Point { x: 1.0, y: 2.0 };
let q = p;  // MOVED — p is now invalid
// println!("{}", p.x);  // ERROR: use after move

Example — after:

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

let p = Point { x: 1.0, y: 2.0 };
let q = p;  // COPIED — p is still valid
println!("{}", p.x);  // Works fine

When NOT to Use

Don't use this when:

  • Type has heap allocation (Vec, String, Box) — can't be Copy
  • Type has a Drop impl — can't be Copy
  • Implicit copying would be expensive or surprising
  • Adding Copy now would be a breaking change to remove later

Anti-pattern

// DON'T: Derive Copy on something that might grow
#[derive(Clone, Copy)]
struct Config {
    port: u16,
    // Later you add: name: String  — BREAKS because String isn't Copy
}

// DO: Only derive Copy on types that are permanently small and stack-only

3. Cow<'a, B> (Clone on Write)

Source:

library/alloc/src/borrow.rs#L169

pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

197 usages in the stdlib.

Why

Cow delays cloning until mutation is needed. If you only read, you pay zero allocation cost (borrowed). If you need to modify, it clones on first write. This is the "avoid allocation in the common case" pattern.

When to Use

Triggers:

  • A function usually returns borrowed data but sometimes needs to allocate (e.g., string escaping — most strings need no change)
  • You want to accept either owned or borrowed without forcing a clone
  • Performance matters and most inputs don't need modification

Example — before:

// Always allocates even when input needs no change
fn escape(s: &str) -> String {
    if s.contains('<') {
        s.replace('<', "&lt;")  // allocates
    } else {
        s.to_string()  // ALSO allocates — unnecessary!
    }
}

Example — after:

use std::borrow::Cow;

fn escape(s: &str) -> Cow<'_, str> {
    if s.contains('<') {
        Cow::Owned(s.replace('<', "&lt;"))  // allocates only when needed
    } else {
        Cow::Borrowed(s)  // zero-cost — just returns a reference
    }
}

When NOT to Use

Don't use this when:

  • You always modify the value (just return the owned type)
  • The lifetime makes the API confusing for callers
  • The performance difference is negligible

4. mem::take / mem::replace (Move Out of &mut)

Source:

library/core/src/mem/mod.rs

112 usages of mem::take/mem::replace in the stdlib.

pub fn take<T: Default>(dest: &mut T) -> T {
    replace(dest, T::default())
}

pub fn replace<T>(dest: &mut T, src: T) -> T {
    // Swaps src into dest, returns old dest
    unsafe { ... }
}

Why

You can't move out of a &mut T directly (would leave the reference dangling). mem::take solves this by replacing with Default and giving you the old value. This is the idiomatic way to "take ownership through a mutable reference."

When to Use

Triggers:

  • You have &mut self and need to move a field out
  • You're implementing state machines (swap states)
  • You need to "reset" a field while extracting its value

Example — before:

struct Parser {
    buffer: Vec<u8>,
}

impl Parser {
    fn flush(&mut self) -> Vec<u8> {
        let result = self.buffer.clone();  // EXPENSIVE clone
        self.buffer.clear();
        result
    }
}

Example — after:

use std::mem;

impl Parser {
    fn flush(&mut self) -> Vec<u8> {
        mem::take(&mut self.buffer)  // moves buffer out, replaces with empty Vec
        // Zero allocations, zero copies
    }
}

When NOT to Use

Don't use this when:

  • The type doesn't implement Default
  • You need to keep the original value (use clone)
  • You're in a consuming method (just take self)

5. Box for Heap Allocation

Source:

library/alloc/src/boxed.rs

697 Box<> usages in library/.

Why

Box is the simplest smart pointer: single-owner heap allocation. Use it when you need a value on the heap with known, fixed ownership. Recursive types REQUIRE boxing (otherwise infinite size).

When to Use

Triggers:

  • Recursive data structures (tree nodes, linked lists)
  • Trait objects (Box<dyn Error>, Box<dyn Fn()>)
  • Large values you want on the heap to avoid stack overflow
  • Transferring ownership without copying large structs

Example — before:

// COMPILE ERROR: recursive type has infinite size
enum Tree {
    Leaf(i32),
    Node(Tree, Tree),  // How big is Tree? Depends on Tree... infinite
}

Example — after:

enum Tree {
    Leaf(i32),
    Node(Box<Tree>, Box<Tree>),  // Box is pointer-sized — finite!
}

When NOT to Use

Don't use this when:

  • The value is small and stack allocation is fine
  • You need shared ownership (use Rc/Arc)
  • You need interior mutability (use RefCell or Mutex)
  • You're boxing just to avoid lifetime annotations (fix the lifetimes)

6. Arc for Shared Ownership Across Threads

Source:

library/alloc/src/sync.rs

798 Rc/Arc usages in library/.

Why

Arc (Atomic Reference Counted) enables multiple owners across threads. The data lives on the heap; cloning an Arc increments the reference count (not the data). Data is freed when the last Arc is dropped.

When to Use

Triggers:

  • Multiple threads need read access to the same data
  • Ownership is shared (no single owner)
  • Combined with Mutex for shared mutable state: Arc<Mutex<T>>

Example — before:

// Can't send &data to another thread — lifetime issue
fn spawn_with_data(data: &Config) {
    std::thread::spawn(|| {
        // ERROR: borrowed data may not outlive the scope
        println!("{}", data.name);
    });
}

Example — after:

use std::sync::Arc;

fn spawn_with_data(data: Arc<Config>) {
    let data = Arc::clone(&data);  // cheap: just increments counter
    std::thread::spawn(move || {
        println!("{}", data.name);  // owns a reference, lives as long as needed
    });
}

When NOT to Use

Don't use this when:

  • Single-threaded (use Rc — no atomic overhead)
  • Single owner is sufficient (use Box)
  • You can restructure to avoid shared ownership (prefer this)
  • You're using it because you can't figure out lifetimes (usually a design smell)

Anti-pattern

// DON'T: Arc everything because lifetimes are hard
struct App {
    db: Arc<Database>,
    cache: Arc<Cache>,
    config: Arc<Config>,
    logger: Arc<Logger>,
}
// If App owns all of these, just use owned fields!
// Arc is for SHARING, not for avoiding lifetime annotations.

7. Drop for Cleanup (RAII)

Source:

library/core/src/ops/drop.rs

254 Drop implementations in the stdlib.

pub trait Drop {
    fn drop(&mut self);
}

Why

Drop runs automatically when a value goes out of scope. This is RAII: resources are tied to ownership. File handles close, locks release, memory frees — all automatically, without try/finally or defer.

When to Use

Triggers:

  • Your type holds an external resource (file, socket, lock)
  • You need cleanup code to run even on panic
  • You're implementing a smart pointer or wrapper

Example — before (in another language):

# Must remember to close — easy to forget on error paths
f = open("data.txt")
try:
    process(f)
finally:
    f.close()  # What if we forget this?

Example — in Rust:

// Drop handles cleanup automatically
{
    let f = File::open("data.txt")?;
    process(&f)?;
}  // f.drop() called here — file closed automatically
   // Even if process() panics, Drop still runs during unwinding

When NOT to Use

Don't use this when:

  • Type doesn't hold external resources (just let memory free)
  • You need Copy (can't impl both Drop and Copy)
  • Cleanup order matters precisely (Drop order is reverse of creation, but this isn't always what you want — use explicit close methods)

8. Lifetime Elision (Let the Compiler Infer)

Source:

The Rust compiler applies 3 elision rules automatically. The stdlib exploits this — most function signatures DON'T write lifetimes:

// Written in source (elided):
fn first_word(s: &str) -> &str { ... }

// What the compiler actually sees:
fn first_word<'a>(s: &'a str) -> &'a str { ... }

1,206 explicit lifetime annotations exist in library/ — only where elision rules don't apply.

Why

Lifetimes are inferred in the common case. Only annotate when:

  • Multiple references in input (compiler can't guess which one the output borrows from)
  • Struct fields that borrow
  • Static or complex relationships

When to Use

Triggers:

  • Function has one reference parameter and returns a reference (rule 1: output gets input's lifetime)
  • Method has &self and returns a reference (rule 3: output gets self's lifetime)

Example — before:

// Unnecessary explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// This NEEDS annotations because there are TWO input references

Example — after:

// Let elision work when it can:
fn trim(s: &str) -> &str { s.trim() }
// No annotations needed — one input ref, one output ref → same lifetime

// Only annotate when the compiler ASKS you to
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }

When NOT to Use

Don't use this when:

  • Multiple input references exist (compiler can't infer which)
  • Struct borrows from multiple sources
  • The relationship is non-obvious (annotate for clarity even if the compiler could infer it)

9. AsRef/Into for Flexible Function Parameters

Source:

library/core/src/convert/mod.rs

79 AsRef implementations in library/.

// std::fs::read accepts anything that can become a Path:
pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> { ... }

Why

AsRef<Path> accepts &str, String, PathBuf, &Path — any type that cheaply references a Path. One function, many input types, zero allocation.

When to Use

Triggers:

  • You want to accept multiple types that can cheaply become &T
  • Ergonomics: let callers pass String or &str interchangeably
  • The conversion is cheap (reference, not allocation)

Example — before:

// Accepts only &Path — callers must convert manually
fn file_exists(path: &Path) -> bool {
    path.exists()
}

file_exists(Path::new("/tmp/foo"));  // works
// file_exists("/tmp/foo");          // ERROR — &str isn't &Path

Example — after:

fn file_exists(path: impl AsRef<Path>) -> bool {
    path.as_ref().exists()
}

file_exists("/tmp/foo");             // &str works
file_exists(String::from("/tmp"));   // String works
file_exists(PathBuf::from("/tmp"));  // PathBuf works

When NOT to Use

Don't use this when:

  • The function is private/internal (just accept the concrete type)
  • The generic bound adds confusion for no real benefit
  • You need ownership, not a reference (use Into<T> instead)

10. PhantomData for Unused Type Parameters

Source:

library/core/src/marker.rs (PhantomData)

401 PhantomData usages in the stdlib.

pub struct PhantomData<T: ?Sized>;

Why

Sometimes you need a type parameter for lifetime or type safety but don't actually store the value. PhantomData tells the compiler "I'm logically related to T" without physically containing T. Used for variance, drop checking, and marker relationships.

When to Use

Triggers:

  • Your struct has a type parameter it doesn't store (for type safety)
  • You need correct variance (covariant, contravariant, invariant)
  • You need the compiler to know your type "owns" a T for drop checking
  • Implementing raw pointer wrappers that should act like &T

Example — before:

// COMPILE ERROR: parameter T is never used
struct TypedId<T> {
    id: u64,
}

Example — after:

use std::marker::PhantomData;

struct TypedId<T> {
    id: u64,
    _phantom: PhantomData<T>,  // tells compiler this is related to T
}

// Now TypedId<User> and TypedId<Product> are different types!
let user_id: TypedId<User> = TypedId { id: 42, _phantom: PhantomData };
let product_id: TypedId<Product> = TypedId { id: 42, _phantom: PhantomData };
// user_id == product_id  // COMPILE ERROR: different types

When NOT to Use

Don't use this when:

  • You can just store the T directly
  • The type parameter doesn't add safety (just remove it)
  • You're cargo-culting PhantomData without understanding variance

Summary: Ownership Decision Tree

Who needs this value?
├── One owner, stack → plain value (no wrapper)
├── One owner, heap → Box<T>
├── Multiple owners, single thread → Rc<T>
├── Multiple owners, multi thread → Arc<T>
└── Need mutation with shared refs?
    ├── Single thread → RefCell<T> (runtime borrow check)
    └── Multi thread → Mutex<T> or RwLock<T>

How to pass to a function?
├── Read only → &T (borrow)
├── Need to modify → &mut T (mutable borrow)
├── Need to store/send → T (ownership transfer)
├── Accept multiple types → impl AsRef<T> or impl Into<T>
└── Might need to clone → Cow<'a, T>

Need to move out of &mut?
└── mem::take (replaces with Default, returns old value)
Pattern Use when
&T parameter Read-only access, no ownership needed
T parameter Must store, send, or consume the value
Clone/Copy Small stack types (Copy), expensive heap types (Clone)
Cow<'a, B> Usually borrowed, sometimes needs to own
mem::take Move a field out of &mut self
Box<T> Heap allocation with single owner
Arc<T> Shared ownership across threads
Drop RAII cleanup (files, locks, connections)
Lifetime elision Let compiler infer when rules apply
AsRef<T> Accept multiple types cheaply
PhantomData<T> Unused type parameter for type safety

See also: