1.5 Lifetimes: how to guarantee a reference points to valid data
In the previous section, we designed a reference (&T) stripped of cleanup responsibility: the original variable s (String) remains solely responsible for freeing the heap block, while a reference &s (&String) stores only the 8-byte stack address of s and never frees the heap when it goes out of scope.
We ended with a nagging question: since a reference does not manage cleanup and the heap is freed whenever the original variable goes out of scope—what happens if the original variable goes out of scope first and frees the heap, while a reference is still sitting in an outer variable waiting to be read?
When the original variable exits first: the address inside the reference dangles
Section titled “When the original variable exits first: the address inside the reference dangles”We don’t even need a function call to trigger this flaw; a simple inner block {} is enough:
{ r; // declare r in the outer block to hold a reference { x = alloc_heap("hello") r = &x // hand the stack address of inner variable x to outer variable r } // ← inner block ends; x goes out of scope right here print(r) // ← follow the address inside r to read: what happens?}Trace this code step by step under the rules of our model so far:
- Inside the inner block, variable
xallocates"hello"on the heap and is responsible for freeing it when the inner{}closes. r = &xcopies the stack address ofxinto the outer variabler. Because a reference is stripped of cleanup duty, responsibility for freeing the heap stays withx.- Execution reaches the inner closing brace
}:xgoes out of scope, firing our scope-exit rule—the three-word control header ofxvanishes from the stack, and the"hello"buffer on the heap is returned to the allocator on the spot. - Back in the outer block, variable
ris still in scope, and its 8 bytes on the stack still hold the old address ofx. Whenprint(r)follows that address on the next line, it reads memory that has already been invalidated.
This is the built-in safety hazard of references—a dangling reference.
Notice how it differs from using a variable after a Move in Section 3: after a Move, the variable itself has been invalidated; here, the reference variable r is still completely in scope—what became invalid is the original variable x that r points to.
Diagnosis: why can’t the runtime catch this?
Section titled “Diagnosis: why can’t the runtime catch this?”Facing the impending memory bug on the line print(r), can the program detect the problem through r and stop it at runtime?
One look at the physical representation of r in memory tells us why it cannot: at runtime, the reference r on the stack is nothing more than a bare 8-byte memory address.
Those 8 bytes hold only a numeric address; not a single bit records whether the x it points to is still alive. When the CPU executes print(r), it blindly follows that address and fetches whatever bytes sit there, with no way to tell whether the target is still valid or was reclaimed when the inner } closed.
If you insisted on preventing this illegal read at runtime, only two old paths would be open:
- Keep
xfrom freeing the heap at}as long asrstill points to it: delay deallocation untilris also done—which hands reclamation timing right back to the runtime (GC or reference counting) and overturns the “no GC” foundation we set in Section 1. - Maintain a runtime validity table for every allocation: check a status flag at runtime before every read through a reference—which adds runtime inspection overhead to every ordinary memory read, violating our performance baseline.
We cannot delay cleanup at runtime, and we cannot insert runtime check instructions on every read. Every runtime path is closed.
That leaves only one conclusion: if we refuse to add runtime overhead, dangling references cannot be solved at runtime at all; the only way out is to have the compiler detect the hazard and reject this code before the program ever runs—at compile time.
The compile-time fix: automatically comparing two valid spans
Section titled “The compile-time fix: automatically comparing two valid spans”If we let the compiler guard against this at compile time, what does it need to inspect in the source code to spot the bug above?
It is remarkably direct—the compiler only needs to compute two spans from the source code and compare them:
- The live span of the original variable
x: from the line wherexis created to the closing brace}of its block. - The use span of the reference
r: fromr = &xwhere it receives the address to the last line that usesr(print(r)).
Lay those two spans side by side as bars, and safety becomes a simple visual check:
- Left panel (valid): the use span of reference
rsits entirely inside the live span of variablex—for as long asris used,xhas not yet gone out of scope, so every read is guaranteed to be safe. - Right panel (dangling): the use span of reference
rreaches past the end ofx—xhas already gone out of scope and freed the heap, yetrtries to read it afterward, so the compiler rejects the program on the spot.
The rule boils down to a single sentence: a reference’s use span must never outlive the live span of the variable it points to.
In Rust, this mechanism—having the compiler track and compare valid spans at compile time—is called lifetimes.
The model is finally a closed loop
Section titled “The model is finally a closed loop”Step back and look at what we have built. With compile-time span checking in place, the automatic memory reclamation model we started assembling in Section 1 has closed its last safety gap:
- When a stack variable goes out of scope (
}), the heap resource it points to is freed automatically—no manualfree, no runtime GC, and no memory leaks from forgotten cleanup. - Assigning a heap-backed value follows Move semantics, invalidating the old variable on the spot—ensuring each heap block has only one variable responsible for freeing it at any moment, eliminating double frees.
- References (
&T) strip away cleanup responsibility, while the compiler checks their valid spans (lifetimes) at compile time—letting functions and multiple readers share data freely under Copy semantics while catching every dangling reference at compile time.
Think back to the apparent paradox at the end of Section 1: if the programmer never writes free and no garbage collector watches memory at runtime, how can a program reclaim memory automatically while staying completely safe?
You now hold the answer in your own hands—because you just derived and invented the entire model from scratch.
The next section concludes Chapter 1. We will step back to assemble the five-step chain we derived into a single panorama, and finally reveal the official Rust names and real compiler diagnostics behind the mechanisms you just designed.