I have a simple database consisting of objects with strings containing unix time as keys and strings containing instructions as values
I want to iterate though the database and erase any object who's key is smaller that current time ( so erase objects with dates before current date)
for (auto it = m_jsonData.begin(); it != m_jsonData.end(); it++) {
if (std::stoi(it.key()) <= (std::time(NULL))) {
std::cout << "command is behind schedule, removing\n";
m_jsonData.erase(it);
} else {
/*
*/
}
}
this code works fine as long as m_jsonData.erase(it); isn't invoked. when it does, in the next iteration std::stoi(it.key()) causes a segfault, after a bit of playing with it I came to a conclusion that is somehow loses track of what it's actually iterating. Is my conclusion true? If not then what is? And how do I fix it?
It's extremely normal for mutating container operations to invalidate iterators. It's one of the first things you should check for.
Documentation for nlohnmann::json::erase():
Notes
Invalidates iterators and references at or after the point of the erase, including the end() iterator.
References and iterators to the erased elements are invalidated. Other references and iterators are not affected.
That means after this line:
m_jsonData.erase(it);
the iterator it can't be used for anything including incrementing it to the next element. It is invalid.
Fortunately, the documentation also points out that the successor to the removed element is returned, so you can just write
for (auto it = m_jsonData.begin(); it != m_jsonData.end(); ) {
if (std::stoi(it.key()) <= (std::time(NULL))) {
it = m_jsonData.erase(it);
} else {
++it;
}
}
Note that when I say this is extremely normal, it's because the standard containers often have similar behaviour. See the documentation for examples, but this is something everyone should be aware of:
std::vector::erase Iterator invalidation
std::unordered_map::erase Iterator invalidation
etc.
This is exactly the reason std::erase was added in C++20, and previously std::remove_if was provided to suppport the erase(remove_if(...), end) idiom, instead of writing fragile mutating loops.
Related
I have been struggling to put a vector object into a project im doing
I have read what little i could find about doing this and decided to give it a go.
std::vector<BrickFalling> fell;
BrickFalling *f1;
I created the vector. This next piece works fine until i get to the erase
section.
if(brickFall == true){
f1 = new BrickFalling;
f1->getBrickXY(brickfallx,brickfally);
fell.push_back(*f1);
brickFall = false;
}
// Now setup an iterator loop through the vector
vector<BrickFalling>::iterator it;
for( it = fell.begin(); it != fell.end(); ++it ) {
// For each BrickFalling, print out their info
it->printBrickFallingInfo(brick,window,deadBrick);
//This is the part im doing wrong /////
if(deadBrick == true)// if dead brick erase
{
BrickFalling[it].erase;//not sure what im supposed to be doing here
deadBrick = false;
}
}
You can totally avoid the issue by using std::remove_if along with vector::erase.
auto it =
std::remove_if(fell.begin(), fell.end(), [&](BrickFalling& b)
{ bool deadBrick = false;
b.printBrickFallingInfo(brick,window,deadBrick);
return deadBrick; });
fell.erase(it, fell.end());
This avoids the hand-writing of the loop.
In general, you should strive to write erasure loops for sequence containers in this fashion. The reason is that it is very easy to get into the "invalid iterator" scenario when writing the loop yourself, i.e. not remembering to reseat your looping iterator each time an erase is done.
The only issue with your code which I do not know about is the printBrickFallingInfo function. If it throws an exception, you may introduce a bug during the erasure process. In that case, you may want to protect the call with a try/catch block to ensure you don't leave the function block too early.
Edit:
As the comment stated, your print... function could be doing too much work just to determine if a brick is falling. If you really are attempting to print stuff and do even more things that may cause some sort of side-effect, another approach similar in nature would be to use std::stable_partition.
With std::stable_partition you can "put on hold" the erasure and just move the elements to be erased at one position in the container (either at the beginning or at the end) all without invalidating those items. That's the main difference -- with std::stable_partition, all you would be doing is move the items to be processed, but the items after movement are still valid. Not so with std::remove and std::remove_if -- moved items are just invalid and any attempt to use those items as if they are still valid is undefined behavior.
auto it =
std::stable_partition(fell.begin(), fell.end(), [&](BrickFalling& b)
{ bool deadBrick = false;
b.printBrickFallingInfo(brick,window,deadBrick);
return deadBrick; });
// if you need to do something with the moved items besides
// erasing them, you can do so. The moved items start from
// fell.begin() up to the iterator it.
//...
//...
// Now we erase the items since we're done with them
fell.erase(fell.begin(), it);
The difference here is that the items we will eventually erase will lie to the left of the partitioning iterator it, so our erase() call will remove the items starting from the beginning. In addition to that, the items are still perfectly valid entries, so you can work with them in any way you wish before you finally erase them.
The other answer detailing the use of remove_if should be used whenever possible. If, however, your situations does not allow you to write your code using remove_if, which can happen in more complicated situations, you can use the following:
You can use vector::erase with an iterator to remove the element at that spot. The iterator used is then invalidated. erase returns a new iterator that points to the next element, so you can use that iterator to continue.
What you end up with is a loop like:
for( it = fell.begin(); it != fell.end(); /* iterator updated in loop */ )
{
if (shouldDelete)
it = fell.erase(it);
else
++it;
}
The values of a std::unordered_set are not mutable, as they are both key and value. What is the correct way to modify an element of a std::unordered_set, if its element are first removed, modified and then reinserted? The erase does not invalidate any iterators, but the insert may. The obvious answer is to use the iterator, that the erase returns. One way, one could deal with this, I guess, is to reset the loop iterator to the beginning of an unordered_set, after a successful insert. I'd like to make sure, it is the only way.
The erase does not invalidate any iterators, but the insert may.
You can always check beforehand if insert is going to do that:
If rehashing occurs due to the insertion, all iterators are
invalidated. Otherwise iterators are not affected. References are not
invalidated. Rehashing occurs only if the new number of elements is
equal to or greater than max_load_factor()*bucket_count().
(from cppreference)
So, if you watch out for rehashing, your approach could work. Of course it leaves you with the problem of what to do when you detect that rehashing will occur.
You can reduce the probability of rehashing by increasing the capacity of the set before the loop.
The simplest way of dealing with rehashing is to start over after rehashing. Maybe there are other ways to deal with it but I wouldn't risk it.
Having said all these, what you are describing here seems to indicate that you probably need another container. If the unordered_set is really the best container for your application, I would most likely still go with Martin's solution, namely with an intermediate container. It's less messy and I can see what's going on; I can reason about correctness.
I think that you are better off using an intermediate container, like this:
unordered_set<int> original;
...
vector<int> temporary;
for (auto it = original.begin(), itEnd = original.end(); it != itEnd; ) {
if (...) {
int newValue = ...;
auto toDelete = it++;
original.erase(toDelete);
temporary.push_back(newValue);
} else {
...
++it;
}
}
original.insert(temporary.begin(), end.begin());
Use a temporary unordered_set that you will fill iterating on the first one (with an hint) then swap the original set with the temporary.
I'm new with STL containers, and right now i'm having some problems working with Multiset.
The problem is with the following two collections:
vector<DataReference*> referenceCol;
multiset<DataCount, DataCountSortingCriterion> orderedCol;
orderedCol mantains some data elements that have two public integer fields: id and count. I'm ordering that structure by the count elements. I may need to increment and decrement the count field from that elements, so, in order to maintain the ordering, i'm using a second collection (referenceCol) which is indexed by the id field and holds a reference (iterator) to the orderedCol collection, so every moment i need to refresh the count i can erase the element from orderedCol quickly (by refering to it in referenceCol), refresh it, and insert it again in its proper place according to the ordering.
The referenceCol is created in the constructor of my class, and has two fields: validReference (bool) that indicates whether the iterator reference is valid or not, and the multiset<....>::iterator variable.
The following methods handle the increment and decrement operations that affect these two collections:
void SomeClass::decrementCount(int index)
{
multiset<DataCount, DataCountSortingCriterion>::iterator it = referenceCol[index]->it;
DataCount dop = *it;
orderedCol.erase(it);
dop.count--;
if (dop.count > 0) {
it = orderedCol.insert(dop);
referenceCol[index]->it = it;
}
else {
referenceCol[index]->validRef = false;
}
}
void SomeClass::incrementCount(int index)
{
DataCount dop;
multiset<DataCount, DataCountSortingCriterion>::iterator it;
if (referenceCol[index]->validRef) {
it = referenceCol[index]->it;
dop = *it;
orderedCol.erase(it); <--------- BOOM!
dop.count++;
}
else {
dop.id = index;
dop.count = 1;
referenceCol[index]->validRef = true;
}
it = orderedCol.insert(dop);
referenceCol[index]->it = it;
}
The problem is that i'm having an error when i try to erase the iterator in the increment operation (look at the BOOM comment from the code).
The error i'm having is this:
"map/set erase iterator outside range"
The only thing that occurs to me is that maybe when erasing elements i may be invalidating other iterators, so those references doesn't hold any more, but i googled it and i found that for multiset, the erase operation only invalidate the erasing elements but no others...
I also checked that in my running example i'm not erasing the element with the problematic index.
Please help! And sorry for my bad english!
Oh, and i'm open to suggestions about better strategies to accomplish the "refresh" of elements in order :)
Thanks in advance!
With only the code you've given us to debug I cannot be certain, but I suspect that you are calling decrementCount(index) such that referenceCol[index]->validRef is false. When this happens your decrementCount method simply calls erase on the iterator without checking validity.
If this were to happen on a formerly invalidated iterator you might see the behavior you're seeing.
As an aside here it appears that you should be using a multimap not a multiset. But again without understanding all of your code I can't say that for sure.
I've got this piece of code:
for (std::vector<Marker>::iterator it = markers.begin(); it != markers.end(); ++it) {
if (it->getDots().size() < 3) {
markers.erase(it);
}
}
In one of test inputs (the app does image analysis) I get a segfault. I tried to debug the code (to no avail) and noticed one thing. When asking gdb to p markers.size() i receive $9 = 3. So I would expect the loop to iterate three times, but surprisingly it does it (at least) 5 times. In fifth iteration there's a segfault. What I also noticed is that it's not the dereference of *it (here it->) that causes the error. It's specifically it->getDots(), which is a simple getter.
I write in C++ very rarely, so it might be some simple mistake, but neither my debugging, nor googling brought any solution. Could you help?
I'd like to emphasize, that on various different inputs (slightly different images) this function works properly, so it's even harder for me to trace the bug down.
vector::erase invalidates all iterators pointing to the element being erased, and all elements that follow. So it becomes invalid, and ++it expression on the next loop iteration exhibits undefined behavior.
The best way to code this logic is with erase-remove idiom.
The problem is this line:
markers.erase(it);
The iterator is invalidated. But that's okay, erase returns a valid iterator:
auto it = markers.begin();
while(it != markers.end())
{
if(it->getDots().size() < 3) {
it = markers.erase(it);
}
else ++it;
}
You need to update it when you erase:
it = markers.erase(it);
since erase will "change" the vector, and your current it is no longer valid (and only do it++ if when you didn't erase it).
However, the more common way to do this is to use this type of construction:
markers.erase(std::remove(markers.begin(), markers.end(), number_in), markers.end());
std::vector<struct::event>::iterator it;
std::vector<struct::event>::iterator last=myvector.end();
for (it=myvector.begin(); it<=last; it++){
if(mysignal.declination<(*last).declination){
if (mysignal.declination>=(*it).declination && mysignal.declination<(*(it+1)).declination){
myvector.insert(it+1, mysignal);
break;
}
}
if (mysignal.declination>=(*last).declination){
myvector.push_back(mysignal);
break;
}
}
I have a vector called myvector with events that are sorted with the declination. now I want to add mysignal to this vector on the right place. but i always get a seg fault after a few events which refers to: if(mysignal.declination<(*last).declination). I just can't see what is wrong.
Your loop is wrong, read the docs:
Returns an iterator to the element following the last element of the container.
This element acts as a placeholder; attempting to access it results in undefined behavior.
You can't dereference end(), it provides a way of knowing that you have overrun the container, so your loop condition should be it != myvector.end(), and last is wrong as well.
As others have said, C++ iterators define a half-open interval
('[begin()...end())'), which is what you should probably be
using in most other cases as well. And although it works with
iterators from a vector, in general, iterators do not support
<= (nor <); the standard idiom for a loop is:
for ( auto current = container.begin();
current != container.end();
++ current ) ...
(In the most likely case that you cannot count on C++11, you'll
have to write out the full iterator type, rather than use
auto. Although auto is one of the few things from C++11
that seems to work with VC++11 and with recent versions of
g++, so if those are the only targets you're concerned with, and
you can be sure of always having very recent versions, you can
use it.)
Also, if you want to access the last element of the vector in
the loop, myvector.back() will return a reference to it.
(myvector.back() is undefined behavior if the vector is empty,
but if the vector is empty, you won't enter the loop.)
end() does not refer to the last element in the container, you need to change your condition as follows.
for (it=myvector.begin(); it != last; it++){
You have other broken logic as well that is dereferencing last that you need to fix.