skip to content

Why can a saved reference into a growable array's storage go stale after later appends?

level: middleimportance: should knowfreq 52%

answer

  1. what does the handle actually name?
  2. one of those appends crossed capacity
  3. growth allocates a new block and copies
  4. the old block is released or reused
  5. order is preserved, addresses are not

basics

~20 s

An append past capacity allocates a larger block, copies the elements over and releases the old one. A reference captured into the old block then names freed storage. Indices survive the move; handles into the buffer do not.

solid answer

~50 s

The reference does not name the element — it names a *slot in the block that existed when you took it*. Appending is a conditional reallocation: most appends write into a free slot, but the one that crosses the capacity threshold allocates a bigger block, copies every element across, and releases the old one. From the call site you cannot tell which kind of append you just made, so the only safe rule is to assume every append may have moved the storage. After a move, a captured reference points into a released block: reading it returns stale values, writing through it corrupts unrelated memory. The common wrong defence — "the array still contains the item, so my handle is good" — confuses containment with addressing. Hold an index instead, or re-derive the handle after the loop.

code

pseudocode · 8 lines
pseudocode
catalog = make-dynamic-array()
append(catalog, first-item)
last = handle-to-slot(catalog, length(catalog) - 1)

for i in 0..n-1:
    append(catalog, incoming[i])   // any one of these may reallocate
...
set-price(last, 9.99)              // writes through the captured slot

go deeper

for a junior

Recall that appending can move the whole block to a new, larger allocation. If you remember that growth means 'allocate, copy, release', you can explain why something pointing at the old storage stops being meaningful.

for a middle

Explain the conditional nature of the reallocation — most appends move nothing, the one that crosses capacity moves everything — and why the call site cannot tell them apart. Be able to contrast index stability with handle stability.

for a senior

Show you can spot this in review and say why it survives testing: small batches never cross a threshold, so the bug is volume-dependent and surfaces far from its cause. Offer the fix that removes the long-lived handle rather than one that depends on a capacity estimate staying correct.

for a principal

Own it as a design rule rather than a bug hunt: decide when your codebase is allowed to hold positions into growable storage at all, and when a structure with stable element addresses is worth its worse locality and higher per-element cost.

## What a handle actually names A growable array stores its elements in one contiguous block. Anything you capture that points *into* that block — a raw address, a cursor, a slice, an iterator — is a coordinate in a specific allocation. It is not a claim on the element's identity. That distinction is the whole question. Appending has two behaviours that look identical at the call site: - **size < capacity** — the element is written into an already-allocated slot. Nothing moves. Existing element addresses stay exactly where they were; only any past-the-end marker advances. - **size == capacity** — a larger block is allocated, every existing element is copied (or moved) into it, and the old block is released. *Every* address into the old block is now dangling. Because the second case is rare and invisible, code that captures a handle and keeps appending usually works in testing — the test batch never crosses a threshold — and fails in production on the batch that does. That is the classic shape of this bug: intermittent, size-dependent, and reproducible only above some input volume. ## The review scenario A catalog-ingest routine appends the item it just created, grabs a handle to "the item we just added" so it can patch a field later, then keeps appending the rest of the incoming batch, then writes through the saved handle. Reviewed casually this looks fine — the item is definitely still in the catalog. But one of the appends in between very likely reallocated, and the write lands in the released block. Depending on the environment, that is undefined behaviour, a silent lost update, or a corrupted neighbouring allocation. The failure is not at the moment of invalidation; it is later, somewhere else, which is why these bugs are expensive. ## Index versus reference — a real difference, not a synonym Reallocation copies elements in order, so **element i is still at position i afterwards**. An index therefore survives a move. This makes indices the standard defensive choice for "remember where that item is" across appends. But an index is a *position*, not an identity. Erasing or inserting in the middle shifts every later element down or up by one, and your saved index now names a different item — with no crash and no warning. So the two are invalidated by *different* operations: | operation | index still names the same element | direct handle into the block still valid | |---|---|---| | append within capacity | yes | yes | | append that reallocates | yes | no | | erase before your position | no | no (positions shifted) | | erase after your position | yes | yes | | explicit shrink | yes | no | If you need something that survives both, you need identity: a stable key on the element itself, or a structure whose elements do not move. ## Ecosystems make different messes of this The consequence of a move depends on whether the block stores elements inline or stores handles to them. Growable arrays in C++ and Rust store elements inline, so reallocation physically relocates the objects and a captured pointer or slice genuinely dangles — which is why both ecosystems specify invalidation rules and why one of them enforces them at compile time. Managed runtimes such as those behind Java and C# store references in the block, so the objects themselves never move: a captured element reference stays a perfectly valid object. The bug there is quieter rather than absent — your reference is now *decoupled* from the container's slot, so mutations through it and mutations through the container can diverge, and a saved position can name a different element entirely. Same conceptual hazard, two very different failure signatures. ## How to write it correctly Four options, in rough order of preference: 1. **Do not hold the handle across appends.** Restructure so the field is set before the element is appended, or re-derive the position after the loop finishes. 2. **Hold an index**, and be explicit that it is only valid while nothing is erased or inserted before it. 3. **Pre-size the storage** for the whole batch, so no reallocation can occur inside the window. This is a real technique but a fragile guarantee — it depends on the estimate being an upper bound, and a future edit that appends one extra element silently reintroduces the bug. 4. **Use storage with stable element addresses** — segmented or node-based structures that never relocate live elements — when addresses genuinely must outlive the container's growth. ## What to say in the room "The handle names a slot in the block that existed when I took it. An append that crosses capacity allocates a new block, copies everything and frees the old one, so the handle is dangling. Indices survive that move because order is preserved, but they do not survive an erase before them." That answer shows you understand *why* the rule exists rather than having memorised a list of invalidating operations.

  • Which is safer to hold across a loop of appends — an index or a direct handle?
    The index. Reallocation copies elements in order, so element i stays at position i and the index still names the same item. But it is only safe against *growth*: any erase or insert before that position shifts everything after it and your index silently names a different element. An index is a position, never an identity.
  • Does an append that stays within capacity invalidate anything?
    Existing element addresses are untouched — nothing moves, only the size counter and any past-the-end marker advance. The problem is that the call site cannot tell which kind of append it just made. Since the threshold-crossing append looks identical to every other, the working rule has to be 'assume it may have moved'.
  • How would you fix the reviewed ingest code?
    Cheapest fix: set the field before appending, so no handle needs to survive the loop. Otherwise store the index and re-derive the handle after the loop, or pre-size for the whole batch so no reallocation can occur in that window. If addresses genuinely must be stable, move to segmented storage whose live elements never relocate.
  • Why do these bugs usually pass code review and testing?
    Because the invalidating append is rare and invisible. Test batches are small enough never to cross a capacity threshold inside the window, so the handle stays valid and the code looks correct. The failure appears only above some input volume, and it surfaces later and elsewhere than the moment of invalidation.

A handle into the block is a desk address; an index is a seat number on the seating chart. When the team relocates to a bigger office the desk address is meaningless, but seat 4 in row 3 still finds the same person — until someone leaves and everyone shuffles up.

saying these in an interview costs you the question

  • The array still holds the item, so my reference is fine
  • Only removals can invalidate references
  • Indices and direct handles are invalidated by the same operations
  • Reallocation copies elements but keeps their addresses
  • It passed the tests, so the handle must be stable

context