3

In a particle system, once particles become old enough, they need to die. Since they are stored in an std::vector the approach that works rather well in XCode is:

for(std::vector<Particle*>::reverse_iterator iter = particles.rbegin(); iter != particles.rend(); ++iter) {  
    (*iter)->update();  
    if ( (*iter)->isDead() ) {
        delete (*iter);
        particles.erase( --iter.base() );  
    }  
}

After booting into Windows, and compiling in Visual Studio 2010, I found that it doesn't work: see here. As the answer itself states, this does not work for associative containers. What I find most frustrating here is that std::reverse_iterator and std::iterator behave differently:

  • .erase does not take a reverse_iterator and wants a real thing (e.g. see this)
  • rev_it.base() call needs to be decremented in the erase call
  • after erasing, I need to convert the std::iterator to an std::reverse_iterator

I thought of using a forward std::iterator but iterating backwards, which is a terrible idea - but the very need for iterating backwards, really, is to ensure that the loop doesn't skip neighboring members of erased particles.

What makes sense to me, though, is not iterating if the .erase() call is made:

for( std::vector<Particle*>::iterator iter = particles.begin(); iter != particles.end(); ) {  
    (*iter)->update();  
    if ( (*iter)->isDead() ) {  
        delete (*iter);
        iter = particles.erase(iter);  
    } else {
        ++iter;
    }
}

This compiles, works, and doesn't seem to be a problem. The question, though, is:

Am I overlooking something that renders this a particularly stupid idea?

(I'm sure that iter will point at the correct next value by taking advantage of the .erase() function's return value, and it seems more readable to me than --iter.base() call.)

That parenthetical aside, a Russian saying that comes to mind is "One who's been burned on hot milk blows on cold water."

Community
  • 1
  • 1
ilzxc
  • 222
  • 1
  • 7
  • Can you afford two passes? One to delete and mark the deleted elements (set to NULL for example), another one to remove NULL elements from the vector? – juanchopanza Jul 05 '13 at 07:34
  • 3
    Can't you just use `std::remove_if`? – n. m. could be an AI Jul 05 '13 at 07:38
  • @juanchopanza Certainly, but I will still need to iterate through the `vector` without skipping any members, otherwise later on I'll be calling `draw()` on NULL elements! – ilzxc Jul 05 '13 at 07:40
  • @n.m. Looking into it now, thanks! – ilzxc Jul 05 '13 at 07:42
  • I'm not sure I understand. You remove the NULL elements in the second pass, so they aren't there anymore to call any methods on. – juanchopanza Jul 05 '13 at 07:43
  • @n.m. I think `std::for_each` first to delete elements and set to NULL, then `std::remove` to remove NULL elements. – juanchopanza Jul 05 '13 at 07:44
  • `std::remove_if` is designed to remove stuff that fulfills a predicate. You are free to disregard the fact and go with a homebrew solution of course, but I fail to see a reason. – n. m. could be an AI Jul 05 '13 at 07:49
  • @n.m. The reason is that we need to call `delete` on elements too. If we simply call `delete` on the elements to be removed, we might have double deletes, since a call to `remove_if` might result in duplicates. The requirement to `delete` changes everything. – juanchopanza Jul 05 '13 at 07:54
  • 1
    @juanchopanza I don't understand what you mean. In the `predicate` function, we delete the object if needed, and then return the proper value so that it gets removed from the vector. In which case would this cause double deletes? – Xaqq Jul 05 '13 at 07:57
  • Don't forget to erase the removed range in the end. – n. m. could be an AI Jul 05 '13 at 08:02
  • @juanchopanza That could happen only if the same pointer was stored multiple times, right? – Xaqq Jul 05 '13 at 08:28
  • @juanchopanza This is however not a problem if `delete`ing the elements directly in the predicate function, *before* they get the chance to be duplicated in any way. – Christian Rau Jul 05 '13 at 08:33
  • @Xaqq I think you're right and I was wrong all along. – juanchopanza Jul 05 '13 at 08:50

3 Answers3

3

Your second piece of code is good. This is also what I do when I need to remove elements from a list I'm iterating over.

AFAIK there's nothing wrong with "manual" control (ie, incrementing the iterator manually) when it's needed. And in your case, it seems needed.

I'm sure that iter will point at the correct next value by taking advantage of the .erase() function's return value, and it seems more readable to me than --iter.base() call.

I totally agree.

EDIT: As @n.m stated in comments, std::remove_if seems pretty adequate in your situation.

Xaqq
  • 4,308
  • 2
  • 25
  • 38
  • Thanks - I'm looking to see if I could use `std::remove_if` if I also need to call `delete` on members of the vector, but it certainly works for collections of objects rather than pointers... – ilzxc Jul 05 '13 at 08:11
3

In addition to the other answers (especially juanchopanza's), you can also do this with a single std::remove_if:

particles.erase(std::remove_if(particles.begin(), particles.end(), 
                               [](Particle *particle) -> bool {
                                   bool dead = p->isDead();
                                   if(dead)
                                       delete p;
                                   return dead;
                               }),
                particles.end());

(feel free to use a custom functor if C++11 lambdas are not available.)

This will work, since the possible duplication of an element will occur after the predicate was evaluated for it and it has already been deleted and therefore the predicate will still get invoked only once for each element in the vector and not for any possible duplicates. What the values after the new end iterator contain is completely irrelevant since we erase them afterwards and std::vector::erase doesn't try to delete anything at all.


EDIT: Another option of course would be to use smart pointers for the particles (in particular C++11's std::unique_ptrs or, if you are well read into the topic and perfectly understand what you are doing, std::shared_ptrs). This would at least free you from the need to manually manage their memory. In this case you could directly map the isDead method to the predicate function, without the need for a lambda at all (and you wouldn't need to modify the range inside the predicate, which is still a bit unidiomatic):

std::vector<std::unique_ptr<Particle>> particles;
...
particles.erase(std::remove_if(particles.begin(), particles.end(), 
                               std::mem_fn(&Particle::isDead)),
                particles.end());

EDIT: Though while we're at it, I cannot avoid asking you the question if those particles need to be allocated dynamically at all and if a std::vector<Particle> might in the end not work equally well (but it may very well be that you have a good reason to use pointers here).

Christian Rau
  • 45,360
  • 10
  • 108
  • 185
  • +1 Looking at your answer, I can't understand what I was worrying about. – juanchopanza Jul 05 '13 at 08:51
  • @juanchopanza Still your two-pass solution might be more idiomatic, since it doesn't misuse a predicate for modifying the range. – Christian Rau Jul 05 '13 at 08:54
  • I knew there had to be *some* kind of value to my solution :) – juanchopanza Jul 05 '13 at 08:56
  • 2
    Nobody here worried that he deletes `p` and then uses `isDead()` from the same pointer? – RedX Jul 05 '13 at 08:58
  • @juanchopanza Your solution works great too :). Also, +1 for mentioning smart pointers, because they're really handy in such situation. RedX, I just submitted an edit. – Xaqq Jul 05 '13 at 08:58
  • @RedX Hah, right! Had it different at first (though for unimportant performance reasons and not for correctness reasons). Thanks for pointing it out. – Christian Rau Jul 05 '13 at 09:05
  • WTF with ppl rejecting an edit that improves the code -_-" (http://stackoverflow.com/review/suggested-edits/2447608) – Xaqq Jul 05 '13 at 09:05
  • 1
    @Xaqq Oh, didn't see this suggestion. Maybe the others thought it was for the same maybe irrelevant perfomance considerations that I had and didn't read *RedX*'s comment to see that it was actually neccessary. Likewise can correcting a wrong answer be interpreted as changing the original post too much, since it might deviate from the original answer too much (though I don't think this applied to your edit). – Christian Rau Jul 05 '13 at 09:08
  • @ChristianRau Yeah sure, but I mean, looking at the code (its not that long) and the edit, you obviously see that its necessary. Well, you edited so it's fine :) – Xaqq Jul 05 '13 at 09:10
  • @ChristianRau The vector contains base and derived object types. The only way I know to get inherited objects to sit in the same vector is to declare the vector as a vector of pointers of the base type, in this case `vector` – ilzxc Jul 06 '13 at 00:07
  • @ilya Ok, dynamic polymorphism is one of the few cases where pointers and dynamic allocation are needed. Still you should think about using `std::unique_ptr`s instead (or at least look at them), if available. – Christian Rau Jul 06 '13 at 07:08
2

I would use a two-pass solution:

1) delete elements and set to NULL:

void killParticle(Particle*& p)
{
  if ( p->isDead() ) {
    delete p;
    p = NULL;  
}  

std::for_each(particles.begin(), particles.end(), killParticle);

2) remove NULL elements using erase-remove idiom:

particles.erase(std::remove(particles.begin(), particles.end(), NULL), 
                particles.end());
juanchopanza
  • 223,364
  • 34
  • 402
  • 480
  • While this would work good, it also imply a complexity of O(2n). A one loop solution would be faster, at least if there's a great number of particles. – Xaqq Jul 05 '13 at 07:52
  • @Xaqq O(2n) is O(n). If I could think of a safe and clean one-loop solution I would have posted it :) – juanchopanza Jul 05 '13 at 07:55
  • Yeah it is, but you still have to iterate twice. – Xaqq Jul 05 '13 at 07:55
  • Thanks a lot! This allows to `delete` as well as call `erase` so this is perfect. – ilzxc Jul 05 '13 at 08:04
  • If you got duplicate pointer, as you are accessing a method (which certainly access private data), you might get a segfault if one of them is dead. – Geod24 Jul 05 '13 at 08:37
  • Feel free to check my answer and explain why it wouldn't work this way. – Christian Rau Jul 05 '13 at 08:43