Ownership Semantics, Move Mechanics & Copy vs Clone

Master affine move semantics, stack memcpy mechanics, static drop tracking, runtime drop flags, and Copy vs Clone tradeoffs.

02. Memory Model, Ownership & Lifetimes7 min read

Memory safety without a garbage collector depends on a single non-negotiable rule: every value in memory has exactly one owner at any given moment.

When ownership transitions from one variable to another, Rust executes a Move. Understanding the difference between a Move, a bitwise Copy, and a deep Clone is the bedrock of systems programming in Rust.

Drop Flag SizeCompiler
1 Byte

When a move is conditional at runtime, rustc allocates a 1-byte stack flag to track drop obligation.

1. Formalizing Move Semantics#

Let SAS_A denote the initialization state of variable AA, and let SBS_B denote the state of variable BB. The move operator Move(A→B)\text{Move}(A \to B) updates compiler state transitions deterministically:

SA↦Uninitialized,SB↦InitializedS_A \mapsto \text{Uninitialized}, \qquad S_B \mapsto \text{Initialized}

Once a variable transitions to Uninitialized, any subsequent read attempt is rejected at compile time as a use-after-move error.

Architecture Diagram
Rendering diagram...

The Drop Glue Destruction Obligation#

When a variable exits its lexical scope, the compiler evaluates its destruction obligation D(x)\mathcal{D}(x):

\text{Execute Destructor (drop)} & \text{if State}(x) = \text{Initialized} \\ \text{No-Op} & \text{if State}(x) = \text{Uninitialized / Moved}

This mathematical guarantee guarantees that memory is freed exactly once, eliminating both memory leaks and double-free vulnerabilities at compile time.

2. Hardware Reality: What is a Move?#

At the hardware CPU level, a Move is simply a shallow byte copy (memcpy) of the stack-allocated header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() { // String header occupies 24 bytes on the stack: // [ptr: 8 bytes, cap: 8 bytes, len: 8 bytes] -> pointing to heap let s1 = String::from("Systems Architecture"); // MOVE OCCURS HERE: // 1. rustc copies 24 bytes from s1 stack slot to s2 stack slot (memcpy). // 2. rustc marks s1 as uninitialized in its static bitvector. // 3. Heap buffer is NOT copied! let s2 = s1; // println!("{}", s1); // COMPILE ERROR: borrow of moved value: `s1` println!("{}", s2); // Valid! }

3. Drop Flags: Static vs Dynamic Tracking#

How does the compiler know whether to call drop when control flow branches dynamically?

Architecture Diagram
Rendering diagram...

When control flow is completely static, drop flags are completely optimized away. The compiler directly emits the destructor instruction at the precise exit point with zero runtime overhead.

4. Copy vs Clone Mechanics#

Not all types transfer ownership when reassigned:

TraitDuplication MechanismHardware CostAllocates Heap?Example Types
CopyImplicit bitwise memcpy of stack bytes.Negligible (L1 cache / registers).Never.i32, f64, bool, [u8; 32], &T.
CloneExplicit user-defined deep duplication via .clone().Proportional to buffer size (O(N)O(N)).Yes (typically).String, Vec<T>, HashMap<K, V>.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Types can implement Copy only if all fields implement Copy // and the type does not implement Drop! #[derive(Debug, Clone, Copy)] struct Point { x: f64, y: f64, } fn main() { let p1 = Point { x: 10.0, y: 20.0 }; let p2 = p1; // Implicit bitwise Copy! // Both p1 and p2 remain fully initialized and usable println!("p1: {:?}, p2: {:?}", p1, p2); }
Partial Moves
When destructuring a struct containing both Copy and non-Copy fields, moving a non-Copy field creates a partial move. The parent struct can no longer be used as a whole, though its remaining initialized fields can still be accessed individually.

5. Concept Verification#

Verify your grasp of Rust ownership and move dynamics:

Concept check

What happens at the hardware level when a 24-byte String is moved to a new variable in Rust?

Concept check

Why can a type that implements the Drop trait never implement the Copy trait?