Containers move: the TArray reference bug
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
- No reallocation happens while there's spare capacity (
Num() < Max()). Add one more element and it breaks. - Freed memory usually stays writable for a while, so the write "succeeds" silently. The crash comes later, somewhere unrelated, when that memory is reused.
- Same failure shape as the GC bug (mock Q3): works in testing, fails later, crashes far from the cause. Naming that shape in an interview is worth as much as the API detail.
Where else this applies
TMap/TSet: adding can rehash and move values.std::vector: identical. Interviewers may ask it as "iterator invalidation".RemoveAtshifts later elements down;RemoveAtSwapmoves the last element into the hole. Either way, stored indices can go stale too.- Adding to an array while looping over it with a range-for: the same bug in disguise.
Fixes
- Store the index, and index at the point of use:
Shapes[Idx].Radius = 50.f;. The normal answer. - Re-acquire the reference after the adds.
Reserve(101)before the loop avoids this reallocation but is fragile. It hides the bug rather than fixing it.- For long-lived references, use stable storage: an array of pointers to individually allocated objects,
TIndirectArray, or UObjects.
"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.