Rust Ownership, Borrowing, and Memory Layout¶
Mutable References vs. Ownership Transfer¶
In Rust you can pass a mutable reference &mut to a function as an argument to allow the function to modify the object. Then the caller can use the object after the function returns. Alternatively it seems like you can pass in the object directly and transfer ownership to the function. Then the function can return the object to move ownership back to the caller.
Is this second approach possible? Is it common? What are the tradeoffs and use cases for each?
Yes, the second approach absolutely works. Here's a quick illustration of both:
// Approach 1: Mutable reference
fn transform_ref(v: &mut Vec<i32>) {
v.push(42);
}
// Approach 2: Move in, move out (sometimes called "pass-by-move" or "take-and-return")
fn transform_owned(mut v: Vec<i32>) -> Vec<i32> {
v.push(42);
v
}
fn main() {
// Approach 1
let mut v1 = vec![1, 2, 3];
transform_ref(&mut v1);
println!("{:?}", v1); // [1, 2, 3, 42]
// Approach 2
let v2 = vec![1, 2, 3];
let v2 = transform_owned(v2);
println!("{:?}", v2); // [1, 2, 3, 42]
}
Both compile and work. But they're not equally idiomatic, and the tradeoffs are real.
&mut T (Mutable Borrow) — The Default Choice¶
This is the standard Rust idiom when a function needs to modify something the caller owns. It's preferred because:
- No ownership transfer overhead in the mental model. The caller retains ownership throughout; the borrow checker enforces the exclusive-access guarantee statically. There's no question about who owns what after the call.
- Composes naturally. You can pass
&mutreferences to multiple functions in sequence without reassignment gymnastics. You can also hold other (non-overlapping) borrows simultaneously. - Works with trait patterns. Most standard library traits that imply mutation (
Write,Read,Extend, etc.) take&mut self, so your code interoperates cleanly. - No return value consumed. The function's return type is free for error handling (
Result<(), E>) or returning computed values, rather than being "used up" returning the object back.
Move-In, Move-Out — When It Makes Sense¶
The take-and-return pattern is less common but has legitimate uses:
- Builder pattern / fluent APIs. This is the canonical case. Methods consume
selfand returnSelf(often a transformed version), enabling chaining:
Each method takes ownership and returns a (possibly different) type. This is idiomatic and widespread.
- Type-state pattern. When a function transforms an object into a different type to enforce state transitions at compile time, ownership transfer is essential:
The caller can't use the UnboundSocket after the call — that's the whole point. The borrow checker enforces the state machine.
-
When the function might need to replace the value entirely. If the function destructs the original and constructs something fundamentally new (not just mutating fields), ownership is cleaner than
&mutwithstd::mem::replacetricks. -
Small
Copytypes. Fori32,f64, etc., this distinction is moot — they're copied, not moved. Nobody passes&mut i32to increment it.
Why Move-In/Move-Out Is Not the Default for Mutation¶
- Ergonomic cost. The caller must rebind:
let v = transform(v);. Forget the rebinding and the value is gone. With&mut, the original binding just works. - Return type pollution. You lose the return channel for meaningful results. If the function also needs to report something, you end up returning tuples:
-> (Vec<i32>, usize), which gets unwieldy. - Doesn't work well with structs that hold the value. If the
Vecis a field of some larger struct, you can't move it out without partially moving the struct (which Rust restricts). You'd needOptionwrapping ormem::take. With&mut, you just pass&mut self.my_vec. - No performance advantage. At the machine level, the
&mutversion passes a pointer. The move version also typically passes a pointer (or memcpys a small header likeVec's 3 words). The optimizer generally makes them equivalent, so there's no speed reason to prefer move-and-return.
Rule of Thumb¶
Use &mut T when the function's purpose is to modify something. Use ownership transfer when the function's purpose is to consume and transform something — especially when the type or identity of the value conceptually changes, or when you're building a fluent/builder API. If you find yourself writing -> T just to "give it back," that's a code smell suggesting &mut T is the right tool.
Mutability Transfers with Ownership¶
In the second example above, v2 in main is a binding to an immutable value, but when we call transform_owned() the value becomes mutable because of the parameter type of transform_owned. Is this correct?
Yes, exactly right. This is a subtle but important point about how Rust thinks about mutability.
Mutability in Rust is a property of the binding, not of the value itself. When you write:
v2 is an immutable binding — you can't write v2.push(4) here. But the Vec on the heap isn't intrinsically "locked." It's just that this particular binding doesn't grant mutation permission.
When you call transform_owned(v2), ownership moves into the function. The original binding v2 is gone — it's not immutable or mutable anymore, it simply doesn't exist. Inside the function:
mut v creates a new mutable binding to that same value. The function now owns it and has declared its binding as mutable, so v.push(42) is legal.
Then on return, ownership moves again into whatever binding the caller provides:
This new v2 is again immutable. You could equally write let mut v2 = ... if you wanted to keep mutating it.
You can see this same principle without any function calls:
let v = vec![1, 2, 3]; // immutable binding
// v.push(4); // won't compile
let mut v = v; // move into new mutable binding (shadowing)
v.push(4); // fine now
The Vec didn't change. The binding's permission changed. Ownership transfer is what makes this possible — once the old binding relinquishes ownership, the new binding is free to declare whatever mutability it wants.
Rust Vocabulary: Bindings, Values, and Places¶
What is the Rust vocabulary for talking about data? For example, if we have:
We know that v2 is a binding. But how do we talk about vec![1, 2, 3]? Is this an "object", or a "variable", or a "value", or something else?
Rust's community and official documentation are fairly deliberate about this terminology.
"Value" is the correct Rust term. The vec![1, 2, 3] expression produces a value — a piece of data with a specific type (Vec<i32>) that lives somewhere in memory. The Rust Reference and The Book both use "value" consistently in this way.
So the precise way to describe let v2 = vec![1, 2, 3]; is: "the binding v2 owns a Vec<i32> value."
A few related terms and how they fit:
-
"Variable" — Rust does use this casually (The Book says "variables and mutability" in Chapter 3), but it's somewhat imprecise because in most languages "variable" conflates the name, the storage, and the data. In Rust, where ownership and borrowing make these distinctions matter, "binding" is more precise for the name and "value" for the data. You'll see both in official docs, but when precision matters, Rustaceans tend toward "binding" + "value."
-
"Object" — Rust generally avoids this term. It carries too much OOP baggage (identity, inheritance, vtables). You'll occasionally see it in lower-level contexts like "the object is dropped" or in the compiler's memory model discussion, but it's not standard vocabulary for everyday Rust. The Reference does use "object" in a few places when discussing raw memory, but "value" is strongly preferred.
-
"Place" — This is the more advanced term you'll encounter. A "place" (or "place expression") refers to a location in memory — it's the "left-hand side" concept. In
let v2 = vec![1, 2, 3];,v2is a place that holds a value. The distinction between place expressions (designate a memory location) and value expressions (produce a value) is how Rust formally thinks about the left-hand vs. right-hand side of assignments. This is also the foundation for understanding why you can take a reference to some expressions but not others.
So the clean mental model is: bindings have names, places have memory locations, and values are the data that inhabit those places.
Stack vs. Heap Allocation by Type¶
Primitives and Fixed-Size Types — Stack Only¶
The entire value lives on the stack in the current frame. When the scope ends, the stack frame is popped and that's it. No allocator involved, no destructor logic. These types implement Copy, so assignment duplicates the bits rather than moving ownership.
Fixed-Size Arrays — Stack Only¶
This is 16 bytes on the stack, inline. The size is known at compile time and baked into the type. Same story — pure stack, Copy if the element type is Copy.
Structs and Enums with Only Fixed-Size Fields — Stack Only¶
struct Point { x: f64, y: f64 }
let p = Point { x: 1.0, y: 2.0 };
enum Direction { North, South, East, West }
let d = Direction::North;
The entire struct or enum is laid out inline on the stack. The size of an enum is the size of its largest variant plus a discriminant tag. No heap involvement unless a field itself is heap-allocated.
Vec<T>, String, HashMap, etc. — Stack Header + Heap Buffer¶
This is the most important category to internalize:
The stack holds a 3-word struct (24 bytes on 64-bit): a pointer to the heap buffer, a length, and a capacity. The actual [1, 2, 3] data lives on the heap. String is identical in layout — it's essentially a Vec<u8> with a UTF-8 invariant.
When v goes out of scope, its Drop implementation frees the heap buffer. The stack portion disappears automatically with the frame.
The term "fat pointer" is sometimes used loosely here, but technically this is just a struct that contains a pointer. Rust reserves "fat pointer" for a more specific thing (below).
Box<T> — Stack Pointer + Heap Value¶
The stack holds a single thin pointer (8 bytes). The value itself — whether it's an i32 or a large array — lives on the heap. Drop frees the heap allocation.
Primary use cases: putting large values on the heap to avoid stack overflow, enabling recursive types (since the compiler needs a known size for each type), and owning trait objects.
Actual Fat Pointers — &[T], &dyn Trait, Box<dyn Trait>¶
Rust uses "fat pointer" specifically for pointers that carry extra metadata alongside the address. There are exactly two cases:
Slice references (&[T], &mut [T], Box<[T]>): two words on the stack — a pointer to the data and a length:
Trait object references (&dyn Trait, Box<dyn Trait>): two words on the stack — a pointer to the data and a pointer to the vtable:
let b: Box<dyn std::fmt::Display> = Box::new(42_i32);
// stack: (ptr to heap i32, ptr to Display vtable for i32)
The vtable itself is a static, compiler-generated table in the read-only data segment — not on the stack or heap.
References (&T, &mut T) — Stack Pointer Only¶
r is a single thin pointer (8 bytes) on the stack, pointing to wherever x lives (also the stack in this case). References never own anything and never trigger deallocation.
Closures — It Depends on Capture¶
A closure is an anonymous struct containing its captured variables. If it captures references, it holds pointers. If it captures by move (move ||), it holds the values themselves. The closure struct itself lives on the stack.
But if you need to erase the type:
Now the closure struct is heap-allocated behind a Box, and the stack holds a fat pointer (data + vtable).
Rc<T> and Arc<T> — Stack Pointer + Heap (Value + Refcount)¶
The stack holds a single thin pointer. The heap holds a block containing the strong count, weak count, and the value (String's 3-word header in this case, which itself points to yet another heap allocation for the string's bytes). So you can end up with two levels of indirection: Rc -> heap block containing String header -> heap buffer containing "hello".
Cloning an Rc just copies the stack pointer and increments the reference count. No deep copy.
Option<T> and Other Enums Wrapping the Above¶
let a: Option<i32> = Some(42); // stack: 8 bytes (value + discriminant, with niche optimization maybe less)
let b: Option<Box<i32>> = Some(Box::new(42)); // stack: 8 bytes! null-pointer optimization
The Option<Box<T>> case is worth highlighting: the compiler knows Box can never be null, so it uses the null bit pattern to represent None. The Option is the same size as the Box — one pointer on the stack. This niche optimization applies to Box, &T, &mut T, NonZero*, and several other types.
Summary Table¶
| Type | Stack Footprint | Heap? |
|---|---|---|
i32, bool, f64 |
the value itself | no |
[T; N] |
N x size_of(T) | no |
| struct (fixed fields) | sum of fields + padding | no |
Vec<T>, String |
3 words (ptr, len, cap) | yes — the buffer |
Box<T> |
1 word (ptr) | yes — the T |
&T, &mut T |
1 word (ptr) | no (borrows existing) |
&[T], &dyn Trait |
2 words (fat ptr) | no (borrows existing) |
Box<dyn Trait> |
2 words (fat ptr) | yes — the T |
Rc<T>, Arc<T> |
1 word (ptr) | yes — refcount + T |
| Closures | captured vars (inline) | no (unless boxed) |
The unifying principle: the stack always holds a fixed, compile-time-known size for every binding. When the actual data is dynamically sized or needs to outlive the frame, it goes on the heap, and the stack holds a pointer (or pointer + metadata) to it.
Mutable Bindings vs. Mutable References¶
The use of the keyword mut is used for both mutable bindings and mutable references and depends on the placement and the use of &mut. Here is a summary of the differences.
This is one of the most common sources of confusion in Rust, because mut appears in several positions and means different things depending on where it sits.
1. Mutable Binding: let mut x¶
let mut x = 5;
x = 10; // OK — the binding allows reassignment
x += 1; // OK — can modify through this binding
let y = 5;
y = 10; // ERROR — immutable binding
This controls whether you can modify or reassign through this particular name. As discussed earlier, mutability is a property of the binding, not the value. The same value can be moved to a mutable or immutable binding:
let v = vec![1, 2, 3]; // immutable binding
let mut v = v; // move to mutable binding, same value
v.push(4); // now legal
For compound types, mut on the binding is all or nothing — you can't make individual fields mutable or immutable the way some languages allow:
let mut p = Point { x: 1.0, y: 2.0 };
p.x = 3.0; // OK — entire struct is mutable through this binding
p.y = 4.0; // OK
2. Mutable Reference: &mut T¶
let mut x = 5;
let r: &mut i32 = &mut x;
*r = 10; // OK — can modify the referent through this reference
println!("{x}"); // x is now 10
This is fundamentally different from a mutable binding. A mutable reference is a borrowed permission to mutate someone else's data. The borrow checker enforces the core rule: you can have either one &mut T or any number of &T references to a value, but never both simultaneously.
The &mut is part of the type — &mut i32 and &i32 are different types, not just different access levels.
3. The Confusing Part: Combining Them¶
Here's where the four combinations matter:
Immutable binding to an immutable reference:
let x = 42;
let r: &i32 = &x;
// Can't modify x through r (*r = 10 won't compile)
// Can't rebind r to point elsewhere (r = &other won't compile)
Mutable binding to an immutable reference:
let x = 42;
let y = 99;
let mut r: &i32 = &x;
r = &y; // OK — can rebind r to point to a different value
// *r = 10; // ERROR — still can't modify the referent
The mut here means you can change where r points, not what it points to. This is analogous to a non-const pointer-to-const in C: const int *p where the pointer variable itself can be reassigned.
Immutable binding to a mutable reference:
let mut x = 42;
let r: &mut i32 = &mut x;
*r = 10; // OK — can modify the referent
// r = &mut other; // ERROR — can't rebind r
You can mutate through the reference, but you can't change which value it points to. This is the most common case — you take a &mut to modify something and don't need to redirect the reference.
Mutable binding to a mutable reference:
let mut x = 42;
let mut y = 99;
let mut r: &mut i32 = &mut x;
*r = 10; // OK — can modify the referent
r = &mut y; // OK — can also rebind r
*r = 50; // OK — now modifying y
Full flexibility on both axes. Relatively rare in practice.
4. mut in Function Parameters¶
Function parameters are just bindings, so the same rules apply:
fn takes_ref(r: &mut i32) {
*r = 42; // can modify the referent
// r = &mut other; // can't rebind r — the binding is immutable
}
fn takes_ref_rebindable(mut r: &mut i32) {
*r = 42; // can modify the referent
// r = &mut other; // could rebind r (unusual but legal)
}
fn takes_value(mut v: Vec<i32>) {
v.push(42); // mut binding lets us mutate the owned value
}
Note that mut on a parameter is purely internal to the function — the caller doesn't see or care whether the function declares its parameter as mut. These compile identically from the caller's perspective:
5. mut in Pattern Matching¶
mut can also appear in destructuring patterns, applying to individual bindings:
let (mut a, b) = (1, 2);
a += 1; // OK — a is a mutable binding
// b += 1; // ERROR — b is immutable
The Mental Model¶
Think of two independent axes:
| Binding is immutable | Binding is mutable (mut x) |
|
|---|---|---|
| Owns a value | Can read, can't modify | Can read and modify |
Holds &T |
Can read referent, can't redirect | Can read referent, can redirect |
Holds &mut T |
Can read/modify referent, can't redirect | Can read/modify referent, can redirect |
The binding's mutability controls whether you can reassign the binding itself. The reference's mutability (& vs &mut) controls whether you can mutate the data being pointed to. They're orthogonal.