Containers move: the TArray reference bug

Mock Q7 · result: Miss · 6 min · "the single most common serious bug in C++, learn it cold"

In one breath

A TArray keeps its elements in one block of memory. When it runs out of room, Add allocates a bigger block, moves everything, and frees the old one. Any reference or pointer you held into the old block now points at freed memory. Rule: a reference into an array is valid only until the array's next modification.

The code from the question

TArray<FShapeParams> Shapes;
Shapes.Add(FShapeParams());
FShapeParams& First = Shapes[0];         // reference into the array's memory
for (int32 i = 0; i < 100; ++i) { Shapes.Add(FShapeParams()); }
First.Radius = 50.0f;                    // dangling reference: undefined behaviour

Each Add past capacity reallocates. First still points into the freed block. The write is undefined behaviour. At best it's lost (the real element is unchanged). At worst it corrupts memory the allocator has since handed to something else.

Why it "works" the first 20 times

Where else this applies

Fixes

  1. Store the index, and index at the point of use: Shapes[Idx].Radius = 50.f;. The normal answer.
  2. Re-acquire the reference after the adds.
  3. Reserve(101) before the loop avoids this reallocation but is fragile. It hides the bug rather than fixing it.
  4. For long-lived references, use stable storage: an array of pointers to individually allocated objects, TIndirectArray, or UObjects.
Say it in an interview (tie it to your work)

"My plugin's shape tree stores pointers to individually allocated UObjects, so a node pointer stays valid when the tree changes. An array-of-structs design would have had exactly this bug."

Quiz yourself

Why does a reference into a TArray break after Add?

If capacity runs out, Add allocates a new block, moves the elements and frees the old block. The reference points into freed memory.

Why might the bug not show up in testing?

No reallocation happens while there's spare capacity, and writing into freed memory often "succeeds" silently until it's reused.

What is Num() vs Max()?

Num = elements in use. Max = capacity allocated. Reallocation happens when Num would go past Max.

Best fix, and why is Reserve alone a weak fix?

Store the index and look it up at use. Reserve only avoids reallocation for that exact count, so the next change to the code brings the bug back.

RemoveAtSwap vs RemoveAt: what goes stale?

RemoveAt shifts every later element down one. RemoveAtSwap moves the last element into the hole (faster, changes order). Both invalidate stored indices for the moved elements.

What's the std:: name for this category of bug?

Iterator (and reference/pointer) invalidation, e.g. std::vector::push_back reallocating.