tags:

views:

1975

answers:

7

Allegedly you cannot just erase/remove an element in a container while iterating as iterator becomes invalid. What are the (safe) ways to remove the elements that meet a certain condition? please only stl, no boost or tr1.

Thanks

EDIT Is there a more elegant way if I want to erase a number of elements that meet a certain criteria, perhaps with using functor and for_each or erase algorithm ?

+8  A: 

Example with std::vector

#include <vector>

using namespace std;

int main()
{

   typedef vector <int> int_vector;

   int_vector v(10);

   // Fill as: 0,1,2,0,1,2 etc
   for (size_t i = 0; i < v.size(); ++i){
      v[i] = i % 3;
   }

   // Remove every element where value == 1    
   for (int_vector::iterator it = v.begin(); it != v.end(); /* BLANK */){
      if (*it == 1){
         it = v.erase(it);
      } else {
         ++it;
      }
   }

}
Viktor Sehr
Didnt know about that, but isnt the iterator returned from erase a new, validated one?Sounds very strange that it would return an invalid iterator?
Viktor Sehr
What would otherwise be the point of returning an iterator?
Viktor Sehr
@j_random_hacker: you are correct that it invalidates any iterators..but std::vector::erase returns a *new*, *valid* iterator to the element after the erased one (or end). This code is perfectly valid.
Evan Teran
I'm sorry, you're absolutely right -- must have misread your code. The iterator returned by erase refers to the next element (23.1.1/7). +1.
j_random_hacker
Deleted my 1st comment so as not to confuse anyone who just reads it... But yes, FTR I wrongly accused Viktor of summoning undefined behaviour :-/
j_random_hacker
Good example, but in order to work for map (and its friends), you can not use the return value of erase() -- for some reason std::map::erase() returns void (MS will mess with your head on this one)
Clay
@Clay: Good point. I checked the standard, and that void return type is mandated there (23.1.2, Table 69), so it's not MS's fault. My guess is that the type is void so as not to force associative containers like map to do expensive lookup operations that will often be discarded. Since all associative containers are required to leave all other iterators valid upon insert() or erase() (23.1.2/8), there's actually no need to sneak a valid iterator back via the return value of erase().
j_random_hacker
Hmm. Clay's point means that unfortunately you can't write a single template function that deletes elements in both sequences (vectors, lists etc.) and associative containers (maps, sets, etc.) -- at least, not without resorting to template metaprogramming/type traits trickery. :/ One more reason to use remove()/remove_if() I guess.
j_random_hacker
+7  A: 

You can as long as you don't invalidate your iterator after you've erased it:

MyContainer::iterator it = myContainer.begin();
while(it != myContainer.end())
{
    if (*it == matchingValue)
    {
       myContainer.erase(it++);
    }
    else
    {
        ++it;
    }
}
Aaron Saarela
+1. "myContainer.erase(it++);" is subtle -- it correctly performs the increment *before* calling erase(), when it is still valid to do so, while passing (a copy of) the *unincremented* iterator to that function.
j_random_hacker
IMPORTANT: That code works for map, set and list, but it will NOT work for vector -- erasing from a vector invalidates iterators to that and all subsequent elements (23.2.4.3/3). Have dropped my +1 for now, will re+1 when you mention this.
j_random_hacker
Are you sure that the increment is before calling erase? AFAIK the increment is guaranteed to be done before the end of the sentence, but the correct order is implementation specific. For example a gnu in debug mode does something like this `v.erase(it), it++`, but in release mode does `tmp = it, ++it, v.erase(tmp)`.
Ismael
@Ismael: Postincrement returns an unmodified *copy* of its operand before incrementing. The STL iterators guarantee this.
greyfade
The post-increment takes place before calling erase(), because the value is required for the call. Erase() gets a copy of the unincremented pointer.
greyfade
@Ismael: function calls are sequence points, so the side effects from the increment are guaranteed to be done before the call to erase begins.
Nate Kohl
+5  A: 
bool IsOdd( int i )
{
    return (i&1)!=0;
}

int a[] = {1,2,3,4,5};
vector<int> v( a, a + 5 );
v.erase( remove_if( v.begin(), v.end(), bind1st( equal_to<int>(), 4 ) ), v.end() );
// v contains {1,2,3,5}
v.erase( remove_if( v.begin(), v.end(), IsOdd ), v.end() );
// v contains {2}
markh44
what is bind1st?
0xC0DEFACE
bind1st creates a function like object that essentially gives you a function call with a constant first parameter - so in the example it would have the effect of equal_to<int>(4, X) where X comes from the sequence that we are iterating over. The effect is that each value in the sequence is compared with 4.
markh44
+1  A: 
template <class Container, class Predicate>
void eraseIf( Container& container, Predicate predicate  ) {
    container.erase( remove_if( container.begin(), container.end(), predicate ), container.end() );
}   

template<class K, class V, class Predicate> 
void eraseIf( map<K,V>& container, Predicate predicate  ) { 
    for(typename map<K,V>::iterator iter=container.begin() ; iter!=container.end() ; ++iter )  {
     if(predicate(iter))
         container.erase(iter);
    }
}
TimW
Doesn't work with a map though.
Gab Royer
now it does ...
TimW
+1  A: 

I prefer version with while:

typedef std::list<some_class_t> list_t;
void f( void ) {
  // Remove items from list
  list_t::iterator it = sample_list.begin();
  while ( it != sample_list.end() ) {
    if ( it->condition == true ) {
      it = sample_list.erase( it );
    } else ++it;    
  }
}

With while there is no danger to increment it twice as it could be in for loop.

Kirill V. Lyadvinsky
A: 

markh44 is the most STL-ish response. Note, however, that in general, iterators are invalidated by modifying the container, but set and map are exceptions. There, you can remove items and still go on using the iterators, except if you delete the very item your iterator is referencing.

Pascal
A: 

Viktor's solution has the upside of being able to do something with the element before removing. (I wasn't able to do this with remove_if or remove_copy_if.) But I prefer to use std::find_if so I never have to increment the iterator myself:

typedef vector<int> int_vector;
int_vector v;

int_vector::iterator itr = v.begin();
for(;;)
{
    itr = std::find_if(itr, v.end(), Predicate(4));
    if (itr == v.end())
    {
        break;
    }

    // do stuff with *itr here

    itr = v.erase(itr);  // grab a new, valid iterator
}

Where Predicate could be bind1st( equal_to<int>(), 4 ) or something like this:

struct Predicate : public unary_function<int, bool>
{
    int mExpected;
    Predicate(int desired) : mExpected(desired) {}
    bool operator() (int input)
    {
        return ( input == mExpected );
    }
};
pydave