¿Puedes eliminar elementos de una lista std :: mientras iteras por ella?

239

Tengo un código que se ve así:

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);

Me gustaría eliminar los elementos inactivos inmediatamente después de actualizarlos, para evitar volver a recorrer la lista. Pero si agrego las líneas comentadas, aparece un error cuando llego a i++: "El iterador de lista no se puede incrementar". Intenté algunas alternativas que no aumentaron en la declaración for, pero no pude hacer que nada funcionara.

¿Cuál es la mejor manera de eliminar elementos mientras recorres una lista estándar?

AShelly
fuente
No he visto ninguna solución basada en iterar hacia atrás. Publiqué uno de esos .
sancho.s ReinstateMonicaCellio

Respuestas:

286

Primero debe incrementar el iterador (con i ++) y luego eliminar el elemento anterior (por ejemplo, utilizando el valor devuelto de i ++). Puede cambiar el código a un ciclo while de esta manera:

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;
    }
}
Michael Kristofik
fuente
77
En realidad, eso no garantiza que funcione. Con "erase (i ++);", solo sabemos que el valor pre-incrementado se pasa a erase (), y el i se incrementa antes del punto y coma, no necesariamente antes de la llamada a erase (). "iterador anterior = i ++; borrar (anterior);" es seguro que el trabajo, como es utilizar el valor de retorno
James Curran
58
No James, i se incrementa antes de llamar a borrar, y el valor anterior se pasa a la función. Los argumentos de una función deben evaluarse completamente antes de que se llame a la función.
Brian Neal
28
@ James Curran: Eso no es correcto. TODOS los argumentos se evalúan completamente antes de llamar a una función.
Martin York
99
Martin York tiene razón. Todos los argumentos de una llamada a función se evalúan completamente antes de llamar a una función, sin excepciones. Así es simplemente cómo funciona la función. Y no tiene nada que ver con su ejemplo foo.b (i ++). C (i ++) (que no está definido en ningún caso)
jalf
75
El uso alternativo i = items.erase(i)es más seguro, porque es equivalente a una lista, pero seguirá funcionando si alguien cambia el contenedor a un vector. Con un vector, borrar () mueve todo a la izquierda para llenar el agujero. Si intenta eliminar el último elemento con código que incrementa el iterador después de borrar, el final se mueve hacia la izquierda y el iterador se mueve hacia la derecha, más allá del final. Y luego te estrellas.
Eric Seppanen
133

Quieres hacer:

i= items.erase(i);

Eso actualizará correctamente el iterador para que apunte a la ubicación después del iterador que eliminó.

MSN
fuente
80
Tenga en cuenta que no puede simplemente colocar ese código en su bucle for. De lo contrario, omitirá un elemento cada vez que elimine uno.
Michael Kristofik
2
¿no puede él hacer yo--; cada vez que sigue su código para evitar saltarse?
Entusiasta del
1
@enthusiasticgeek, ¿qué pasa si i==items.begin()?
MSN
1
@enthusiasticgeek, en ese momento deberías hacerlo i= items.erase(i);. Es la forma canónica y ya se encarga de todos esos detalles.
MSN
66
Michael señala un gran 'problema', tuve que lidiar con lo mismo en este momento. El método más fácil para evitarlo que encontré fue descomponer el bucle for () en un momento () y tener cuidado con el incremento
Anne Quinn
22

Debe hacer la combinación de la respuesta de Kristo y MSN:

// 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 != 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);
    }
}

Por supuesto, lo más eficiente y más inteligente de SuperCool® STL sería algo como esto:

// 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)));
Miguel
fuente
Consideré su método SuperCool, mi duda fue que la llamada a remove_if no deja en claro que el objetivo es procesar los elementos, en lugar de eliminarlos de la lista de activos. (Los elementos no se eliminan porque solo se vuelven inactivos, no innecesarios)
AShelly
Supongo que tienes razón. Por un lado, me inclino a sugerir cambiar el nombre de 'actualizar' para eliminar la oscuridad, pero la verdad es que este código es elegante para los functores, pero también es todo menos oscuro.
Mike
Comentario justo, arregle el bucle while para usar end o elimine la definición no utilizada.
Mike
10

Utilice el algoritmo std :: remove_if.

Editar: El trabajo con colecciones debe ser como: 1. preparar la colección. 2. proceso de recogida.

La vida será más fácil si no mezclas estos pasos.

  1. std :: remove_if. o list :: remove_if (si sabe que trabaja con list y no con TCollection)
  2. std :: for_each
Mykola Golubyev
fuente
2
std :: list tiene una función miembro remove_if que es más eficiente que el algoritmo remove_if (y no requiere el idioma "remove-erase").
Brian Neal
5

Aquí hay un ejemplo usando un forbucle que itera la lista e incrementa o revalida el iterador en caso de que un elemento sea eliminado durante el recorrido de la lista.

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);
David Cormack
fuente
4

La alternativa para la versión en bucle a la respuesta de Kristo.

Pierde algo de eficiencia, retrocede y luego avanza nuevamente al eliminar, pero a cambio del incremento de iterador adicional, puede hacer que el iterador se declare en el alcance del bucle y que el código se vea un poco más limpio. Qué elegir depende de las prioridades del momento.

La respuesta fue totalmente fuera de tiempo, lo sé ...

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);
    }
}
Rafael Gago
fuente
1
Eso es lo que he usado también. Pero no estoy seguro de si está garantizado que funcione si el elemento que se eliminará es el primer elemento en el contenedor. Para mí, funciona, creo, pero no estoy seguro de si es portátil en todas las plataformas.
trololo
No hice "-1", sin embargo, ¿el iterador de lista no puede disminuir? Al menos tengo una afirmación de Visual Studio 2008.
milesma
Siempre que la lista vinculada se implemente como una lista circular doble vinculada que tenga un nodo head / stub (usado como end () rbegin () y cuando esté vacío, se usará como begin () y rend () también) esto funcionará. No recuerdo en qué plataforma estaba usando esto, pero también estaba funcionando para mí, ya que la implementación mencionada anteriormente es la implementación más común para una lista std ::. Pero de todos modos, es casi seguro que esto estaba explotando un comportamiento indefinido (según el estándar C ++), así que mejor no lo use.
Rafael Gago
re: iterator cannot be decrementedEl erasemétodo necesita a random access iterator. Algunas implementaciones de colecciones proporcionan un elemento forward only iteratorque causa la aserción.
Jesse Chisholm
@Jesse Chisholm, la pregunta era sobre una lista std ::, no un contenedor arbitrario. std :: list proporciona iteradores de borrado y bidireccionales.
Rafael Gago
4

Lo resumí, aquí está el método tres con ejemplo:

1. usando el whilebucle

list<int> lst{4, 1, 2, 3, 5};

auto it = lst.begin();
while (it != lst.end()){
    if((*it % 2) == 1){
        it = lst.erase(it);// erase and go to next
    } else{
        ++it;  // go to next
    }
}

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

2. usando la remove_iffunción miembro en la lista:

list<int> lst{4, 1, 2, 3, 5};

lst.remove_if([](int a){return a % 2 == 1;});

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

3. usando la std::remove_iffunción que combina con la erasefunción miembro:

list<int> lst{4, 1, 2, 3, 5};

lst.erase(std::remove_if(lst.begin(), lst.end(), [](int a){
    return a % 2 == 1;
}), lst.end());

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

4. usando el forbucle, debería tener en cuenta actualizar el iterador:

list<int> lst{4, 1, 2, 3, 5};

for(auto it = lst.begin(); it != lst.end();++it){
    if ((*it % 2) == 1){
        it = lst.erase(it);  erase and go to next(erase will return the next iterator)
        --it;  // as it will be add again in for, so we go back one step
    }
}

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2 
Jayhello
fuente
2

La eliminación invalida solo los iteradores que apuntan a los elementos que se eliminan.

Entonces, en este caso, después de eliminar * i, se invalida y no puede hacer un incremento en él.

Lo que puede hacer es primero guardar el iterador del elemento que se va a eliminar, luego incrementar el iterador y luego eliminar el guardado.

anand
fuente
2
Usar post-incremento es mucho más elegante.
Brian Neal
2

Si piensa en std::listuna cola, puede quitar y poner en cola todos los elementos que desea conservar, pero solo quitar (y no poner en cola) el elemento que desea eliminar. Aquí hay un ejemplo donde quiero eliminar 5 de una lista que contiene los números 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 ahora solo tendrá los números 1-4 y 6-10.

Alex Bagg
fuente
Enfoque interesante, pero me temo que puede ser lento.
sg7
2

Iterar hacia atrás evita el efecto de borrar un elemento en los elementos restantes a atravesar:

typedef list<item*> list_t;
for ( list_t::iterator it = items.end() ; it != items.begin() ; ) {
    --it;
    bool remove = <determine whether to remove>
    if ( remove ) {
        items.erase( it );
    }
}

PD: vea esto , por ejemplo, con respecto a la iteración hacia atrás.

PS2: no probé a fondo si maneja bien los elementos de borrado en los extremos.

sancho.s ReinstateMonicaCellio
fuente
re: avoids the effect of erasing an element on the remaining elementspara una lista, probablemente sí. Para un vector tal vez no. Eso no es algo garantizado en colecciones arbitrarias. Por ejemplo, un mapa puede decidir reequilibrarse a sí mismo.
Jesse Chisholm
1

Puedes escribir

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++;
    }
}

Puede escribir código equivalente con std::list::remove_if, que es menos detallado y más explícito

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

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

El std::vector::erase std::remove_ifidioma debe usarse cuando los elementos son un vector en lugar de una lista para mantener la competencia en O (n), o en caso de que escriba un código genérico y los elementos podrían ser un contenedor sin una forma efectiva de borrar elementos individuales (como un vector)

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;
}));
J. Kalz
fuente
-4

Creo que tienes un error allí, lo codifico de esta manera:

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);
}
Marcin Skoczylas
fuente
Supongo que esto fue rechazado por preferencias de estilo, ya que parece ser funcional. Sí, claro, (1) usa un iterador adicional, (2) el incremento del iterador está en un lugar extraño para un bucle sin una buena razón para colocarlo allí, (3) hace que el canal funcione después de la decisión de eliminarlo de antes como en el OP. Pero no es una respuesta incorrecta.
Jesse Chisholm