We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Containers, Strings, Algorithms step 5 of 5
Erasing while you walk
Every container promises that its iterators, pointers and references stay valid — until you do something that breaks them. Those promises are specific, and they are the reason this is a rule and not a superstition.
| Container | What invalidates what |
|---|---|
vector |
growth past capacity invalidates everything; erase invalidates from the erased position onward |
deque |
insert or erase in the middle invalidates everything |
map, set, list |
only the erased element; everything else survives |
unordered_map |
rehashing invalidates all iterators, but references to elements survive |
The vector row is the one to internalise, and it is what Track 4 promised
to come back to:
int* p = &values[0];
values.push_back(x); // may reallocate
*p = 1; // p points into freed memory
A vector holds one contiguous block. Growing past its capacity means a new block and a relocation, and every pointer, reference and iterator into the old one is now pointing at memory that has been freed.
The erase-while-looping bug
This one does not even need undefined behaviour to be wrong:
for (std::size_t i = 0; i < values.size(); ++i) {
if (unwanted(values[i])) {
values.erase(values.begin() + i);
}
}
Erasing at i shifts everything after it down by one, so the element that
moves into position i is then skipped by ++i. Two unwanted values in a
row, and the second one survives. Perfectly defined, entirely wrong, and it
passes any test whose unwanted elements happen not to be adjacent.
The iterator version of the same loop is worse, because it is undefined
rather than merely incorrect. erase returns the iterator to the element
after the one removed, precisely so that a loop can be written correctly:
for (auto it = v.begin(); it != v.end(); ) {
if (unwanted(*it)) {
it = v.erase(it); // erase hands back the next valid iterator
} else {
++it;
}
}
That is correct. It is also O(n²) for a vector — each erase shifts the
entire tail — and it is more moving parts than the job needs.
What to write instead
C++20:
std::erase_if(values, unwanted); // returns how many were removed
One pass, one shift, and the count comes back for free.
Before C++20 it was the erase-remove idiom, which you will still meet constantly:
values.erase(std::remove_if(values.begin(), values.end(), unwanted), values.end());
remove_if does not remove anything — it cannot, since an algorithm only
has iterators and no container to shrink. It shuffles the keepers to the
front and returns the iterator marking the new end; the container’s erase
then drops the tail. Both halves are required, and forgetting the second one
leaves the vector its original length with unspecified values at the back.
Your task
int purge(std::vector<int>& values, int divisor);
Remove every value that is exactly divisible by divisor, in place,
preserving the order of the rest. Return how many were removed. A divisor
of zero removes nothing — division by zero is undefined behaviour, not an
exception, so it must not be reached.
The starter erases as it walks and skips whatever moves up behind it.
Stuck?
C++ reference solution
Sign in to attempt this problem and reveal the reference solution.