2009-02-27 6 views
204

J'ai le code qui ressemble à ceci:Pouvez-vous supprimer des éléments d'une liste std :: en l'itérant?

for (std::list<item*>::iterator i=items.begin();i!=items.end();i++) 
{ 
    bool isActive = (*i)->update(); 
    //if (!isActive) 
    // items.remove(*i); 
    //else 
     other_code_involving(*i); 
} 
items.remove_if(CheckItemNotActive); 

Je voudrais supprimer des éléments inactifs immédiatement après leur mise à jour, afinde pour éviter de marcher à nouveau la liste. Mais si j'ajoute les lignes commentées, j'obtiens une erreur quand j'arrive à i++: "Liste l'itérateur non incrémentable". J'ai essayé quelques alternatives qui n'ont pas été incrémentées dans la déclaration for, mais je n'ai rien pu faire.

Quelle est la meilleure façon de supprimer des éléments lorsque vous marchez dans une liste std :: list?

Répondre

227

Vous devez d'abord incrémenter l'itérateur (avec i ++), puis supprimer l'élément précédent (par exemple, en utilisant la valeur renvoyée par i ++). Vous pouvez modifier le code à une boucle while comme ceci:

std::list<item*>::iterator i = items.begin(); 
while (i != items.end()) 
{ 
    bool isActive = (*i)->update(); 
    if (!isActive) 
    { 
     items.erase(i++); // alternatively, i = items.erase(i); 
    } 
    else 
    { 
     other_code_involving(*i); 
     ++i; 
    } 
} 
+0

parfait, travaillé comme un charme. – AShelly

+4

En fait, ce n'est pas garanti de travailler. Avec "erase (i ++);", nous savons seulement que la valeur pré-incrémentée est passée à erase(), et que i est incrémenté avant le point-virgule, pas nécessairement avant l'appel à erase(). "itérateur prev = i ++; effacer (prev);" est sûr de travailler, comme est l'utilisation de la valeur de retour –

+38

Non James, i est incrémenté avant d'appeler l'effacement, et la valeur précédente est passée à la fonction. Les arguments d'une fonction doivent être entièrement évalués avant l'appel de la fonction. –

102

Vous voulez faire:

i= items.erase(i); 

Ce sera correctement mise à jour du iterator pour pointer vers l'emplacement après l'itérateur que vous avez retiré.

+64

Soyez averti que vous pouvez ' Il suffit de déposer ce code dans votre boucle for. Sinon, vous ignorerez un élément chaque fois que vous en supprimerez un. –

+1

peut-il pas faire i--; chaque fois en suivant son morceau de code pour éviter de sauter? – enthusiasticgeek

+1

@enthusiasticgeek, que se passe-t-il si 'i == items.begin()'? – MSN

9

Utilisez l'algorithme std :: remove_if.

Edit: Travailler avec des collections devrait être comme: 1. préparer collection. 2. collection de processus.

La vie sera plus facile si vous ne mélangez pas ces étapes.

  1. std :: remove_if. ou d'une liste :: remove_if (si vous savez que vous travaillez avec la liste et non avec le TCollection)
  2. std :: for_each
+1

incrémentée, la fonction membre remove_if est plus efficace que l'algorithme remove_if (et non exiger l'idiome "remove-erase"). –

2

Suppression Invalide seulement les itérateurs qui pointent vers les éléments qui sont supprimés.

Donc, dans ce cas, après avoir supprimé * i, i est invalidé et vous ne pouvez pas incrémenter.

Ce que vous pouvez faire est d'enregistrer d'abord l'itérateur de l'élément à supprimer, puis d'incrémenter l'itérateur, puis de supprimer celui qui a été enregistré.

+2

L'utilisation de post-incrément est beaucoup plus élégante. –

18

Vous devez faire la combinaison de la réponse de Kristo et MSN de:

// Note: Using the pre-increment operator is preferred for iterators because 
//  there can be a performance gain. 
// 
// Note: As long as you are iterating from beginning to end, without inserting 
//  along the way you can safely save end once; otherwise get it at the 
//  top of each loop. 

std::list< item * >::iterator iter = items.begin(); 
std::list< item * >::iterator end = items.end(); 

while (iter != items.end()) 
{ 
    item * pItem = *iter; 

    if (pItem->update() == true) 
    { 
     other_code_involving(pItem); 
     ++iter; 
    } 
    else 
    { 
     // BTW, who is deleting pItem, a.k.a. (*iter)? 
     iter = items.erase(iter); 
    } 
} 

Bien sûr, le plus efficace et SuperCool ® chose STL savy serait quelque chose comme ceci:

// This implementation of update executes other_code_involving(Item *) if 
// this instance needs updating. 
// 
// This method returns true if this still needs future updates. 
// 
bool Item::update(void) 
{ 
    if (m_needsUpdates == true) 
    { 
     m_needsUpdates = other_code_involving(this); 
    } 

    return (m_needsUpdates); 
} 

// This call does everything the previous loop did!!! (Including the fact 
// that it isn't deleting the items that are erased!) 
items.remove_if(std::not1(std::mem_fun(&Item::update))); 
+0

J'ai considéré votre méthode SuperCool, mon hésitation était que l'appel à remove_if ne précise pas que l'objectif est de traiter les éléments, plutôt que de les retirer de la liste des actifs. (Les éléments ne sont pas supprimés car ils ne deviennent inactifs, pas inutiles) – AShelly

+0

Je suppose que vous avez raison. D'une part je suis enclin à suggérer de changer le nom de 'mise à jour' pour supprimer l'obscurité, mais la vérité est, ce code est fantaisie avec les foncteurs, mais il est aussi tout sauf non-obscur. – Mike

+0

vous définissez 'itérateur fin 'mais ne l'utilisez jamais ... – JHBonarius

4

L'alternative pour la version en boucle à la réponse de Kristo. Vous perdez un peu d'efficacité, vous revenez en arrière puis vous réacheminez lors de la suppression, mais en échange de l'incrément supplémentaire de l'itérateur, vous pouvez faire déclarer l'itérateur dans la portée de la boucle et le code un peu plus propre. Ce qu'il faut choisir dépend des priorités du moment.

La réponse était totalement hors délai, je sais ...

typedef std::list<item*>::iterator item_iterator; 

for(item_iterator i = items.begin(); i != items.end(); ++i) 
{ 
    bool isActive = (*i)->update(); 

    if (!isActive) 
    { 
     items.erase(i--); 
    } 
    else 
    { 
     other_code_involving(*i); 
    } 
} 
+1

C'est aussi ce que j'ai utilisé. Mais je ne suis pas sûr que cela fonctionne si l'élément à supprimer est le premier élément du conteneur. Pour moi, ça fonctionne, je pense, mais je ne sais pas si c'est portable sur plusieurs plateformes. – trololo

+0

Je n'ai pas fait "-1", cependant, l'itérateur de liste ne peut pas décrémenter? Au moins j'ai l'assertion de Visual Studio 2008. – milesma

+0

Tant que la liste liée est implémentée comme une liste circulaire double liée ayant un nœud tête/stub (utilisé comme end() rbegin() et quand vide utilisé comme begin() et rend() aussi) cela fonctionnera. Je ne me souviens pas de la plateforme sur laquelle je l'utilisais, mais cela fonctionnait aussi pour moi, car l'implémentation nommée ci-dessus est l'implémentation la plus courante pour une liste std :: list. Mais de toute façon, il est presque certain que cela exploitait un comportement non défini (par le standard C++), mieux vaut ne pas l'utiliser. –

-4

Je pense que vous avez un bug là, je code ainsi:

for (std::list<CAudioChannel *>::iterator itAudioChannel = audioChannels.begin(); 
      itAudioChannel != audioChannels.end();) 
{ 
    CAudioChannel *audioChannel = *itAudioChannel; 
    std::list<CAudioChannel *>::iterator itCurrentAudioChannel = itAudioChannel; 
    itAudioChannel++; 

    if (audioChannel->destroyMe) 
    { 
     audioChannels.erase(itCurrentAudioChannel); 
     delete audioChannel; 
     continue; 
    } 
    audioChannel->Mix(outBuffer, numSamples); 
} 
4

Voici un exemple en utilisant une boucle for qui parcourt la liste et incréments ou revalide l'itérateur en cas d'un élément étant retiré pendant la traversée de la liste.

for(auto i = items.begin(); i != items.end();) 
{ 
    if(bool isActive = (*i)->update()) 
    { 
     other_code_involving(*i); 
     ++i; 

    } 
    else 
    { 
     i = items.erase(i); 

    } 

} 

items.remove_if(CheckItemNotActive); 
2

Si vous pensez de la std::list comme une file d'attente, vous pouvez dequeue et enqueue tous les éléments que vous souhaitez conserver, mais seulement dequeue (et non enqueue) l'élément que vous souhaitez supprimer. Voici un exemple où je veux supprimer 5 à partir d'une liste contenant les numéros 1-10 ...

std::list<int> myList; 

int size = myList.size(); // The size needs to be saved to iterate through the whole thing 

for (int i = 0; i < size; ++i) 
{ 
    int val = myList.back() 
    myList.pop_back() // dequeue 
    if (val != 5) 
    { 
     myList.push_front(val) // enqueue if not 5 
    } 
} 

myList auront maintenant que des chiffres 1-4 et 6-10.

2

Vous pouvez écrire

std::list<item*>::iterator i = items.begin(); 
while (i != items.end()) 
{ 
    bool isActive = (*i)->update(); 
    if (!isActive) { 
     i = items.erase(i); 
    } else { 
     other_code_involving(*i); 
     i++; 
    } 
} 

Vous pouvez écrire un code équivalent std::list::remove_if, moins bavard et plus explicite

items.remove_if([] (item*i) { 
    bool isActive = (*i)->update(); 
    if (!isActive) 
     return true; 

    other_code_involving(*i); 
    return false; 
}); 

L'idiome std::vector::erasestd::remove_if doit être utilisé lorsque des éléments est un vecteur au lieu de une liste pour garder compexity à O (n) - ou dans le cas où vous écrivez du code générique et des éléments peuvent être un conteneur sans moyen efficace d'effacer des éléments simples (comme un vecteur)

items.erase(std::remove_if(begin(items), end(items), [] (item*i) { 
    bool isActive = (*i)->update(); 
    if (!isActive) 
     return true; 

    other_code_involving(*i); 
    return false; 
})); 
Questions connexes