¿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?


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

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ó.


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

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

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

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

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 

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.


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.


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.


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

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

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.