39 Intermediate The Hundred
push_back During the Loop
the to-do list that ate itself
#include <iostream>
#include <vector>
int main() {
std::vector<int> tasks{10, 20, 30};
std::cout << "size " << tasks.size() << ", capacity " << tasks.capacity() << "\n";
for (int t : tasks) {
std::cout << "handling " << t << "\n";
if (t == 10)
tasks.push_back(99); // task 10 spawns a follow-up task
}
}
Run it. Which tasks get handled?
Answer
Only 10. Typical output (GCC on x86-64): handling 10, handling 0, then
handling 1258427999 — a different garbage number every run. Tasks 20, 30 and 99
never appear.
Why¶
A range-for is sugar for auto __b = tasks.begin(), __e = tasks.end(); — both captured
once, before the body ever runs. {10, 20, 30} allocates exactly three slots (hence
capacity 3), so the first push_back must reallocate: new buffer, elements moved over,
old buffer freed. The loop's cached iterators still point into the freed block, and every
dereference after that is undefined behavior — the 0 and the ever-changing number
are the allocator's own bookkeeping, scribbled over the block as it was freed. With spare
capacity the loop may even appear to work, but it is UB all the same: push_back always
invalidates end(), the exact iterator the loop compares against. The same reallocation
also snaps every pointer and reference into the vector — an int& first = tasks[0] taken
before the push_back goes stale the same way. Neither -Wall -Wextra nor -O2 says a
word: the compiler cannot see this one for you.
The fix¶
Iterate by index, with a snapshot of the size:
const std::size_t n = tasks.size(); // snapshot: walk only the originals
for (std::size_t i = 0; i < n; ++i) {
std::cout << "handling " << tasks[i] << "\n";
if (tasks[i] == 10)
tasks.push_back(99);
}
Indexes survive reallocation because tasks[i] asks the vector afresh each time; use
i < tasks.size() instead if follow-ups should be handled too. reserve() keeps
references valid but does not legalize the range-for — end() is invalidated anyway.
cppreference's iterator-invalidation table
covers every container.
Takeaway: a range-for grabs begin/end once — grow the container and they dangle;
a loop that feeds its own container must use indexes.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo