web-dev-qa-db-fra.com

Suppression d'un élément du vecteur alors qu'elle se trouve dans la plage C++ 11 'pour' boucle?

J'ai un vecteur de IInventory *, et je parcours la liste en utilisant la plage C++ 11 pour, faire des choses avec chacun d'eux.

Après avoir fait quelques choses avec un, je peux vouloir le retirer de la liste et supprimer l'objet. Je sais que je peux appeler delete sur le pointeur à tout moment pour le nettoyer, mais quelle est la bonne façon de le supprimer du vecteur, alors qu'il se situe dans la boucle for? Et si je le retire de la liste, ma boucle sera-t-elle invalidée?

std::vector<IInventory*> inv;
inv.Push_back(new Foo());
inv.Push_back(new Bar());

for (IInventory* index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
}
81
EddieV223

Non, tu ne peux pas. for basé sur une plage est indiqué lorsque vous devez accéder à chaque élément d'un conteneur une fois.

Vous devez utiliser la boucle for normale ou l’un de ses cousins ​​si vous devez modifier le conteneur au fur et à mesure, accéder à un élément plusieurs fois ou, sinon, effectuer une itération non linéaire dans le conteneur.

Par exemple:

auto i = std::begin(inv);

while (i != std::end(inv)) {
    // Do some stuff
    if (blah)
        i = inv.erase(i);
    else
        ++i;
}
75
Seth Carnegie

Chaque fois qu'un élément est supprimé du vecteur, vous devez supposer que les itérateurs situés à ou après l'élément supprimé ne sont plus valides, car chacun des éléments succédant à l'élément supprimé est déplacé.

Une boucle for basée sur une plage est simplement un sucre syntaxique pour une boucle "normale" utilisant des itérateurs, de sorte que ce qui précède s'applique.

Cela étant dit, vous pouvez simplement:

inv.erase(
    std::remove_if(
        inv.begin(),
        inv.end(),
        [](IInventory* element) -> bool {
            // Do "some stuff", then return true if element should be removed.
            return true;
        }
    ),
    inv.end()
);
47

Idéalement, vous ne devriez pas modifier le vecteur en le parcourant. Utilisez l'idiome effacer-supprimer. Si vous le faites, vous êtes susceptible de rencontrer quelques problèmes. Étant donné que, dans vector, erase invalide tous les itérateurs commençant par l’élément effacé jusqu’à la end(), vous devez vous assurer que vos itérateurs restent valides en utilisant:

for (MyVector::iterator b = v.begin(); b != v.end();) { 
    if (foo) {
       b = v.erase( b ); // reseat iterator to a valid value post-erase
    else {
       ++b;
    }
}

Notez que vous avez besoin du test b != v.end() en l'état. Si vous essayez de l'optimiser comme suit:

for (MyVector::iterator b = v.begin(), e = v.end(); b != e;)

vous rencontrerez UB puisque votre e est invalidé après le premier appel erase.

11
dirkgently

Est-ce une obligation stricte de supprimer des éléments dans cette boucle? Sinon, vous pouvez définir les pointeurs que vous souhaitez supprimer sur NULL et effectuer un autre passage sur le vecteur pour supprimer tous les pointeurs NULL.

std::vector<IInventory*> inv;
inv.Push_back( new Foo() );
inv.Push_back( new Bar() );

for ( IInventory* &index : inv )
{
    // do some stuff
    // ok I decided I need to remove this object from inv...?
    if (do_delete_index)
    {
        delete index;
        index = NULL;
    }
}
std::remove(inv.begin(), inv.end(), NULL);
6
Yexo

Je vais montrer avec exemple, l'exemple ci-dessous supprime les éléments impairs du vecteur:

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};

    //method 1
    for(auto it = vecInt.begin();it != vecInt.end();){
        if(*it % 2){// remove all the odds
            it = vecInt.erase(it);
        } else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 2
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 2
    for(auto it=std::begin(vecInt);it!=std::end(vecInt);){
        if (*it % 2){
            it = vecInt.erase(it);
        }else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 3
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 3
    vecInt.erase(std::remove_if(vecInt.begin(), vecInt.end(),
                 [](const int a){return a % 2;}),
                 vecInt.end());

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

}

sortie aw ci-dessous:

024
024
024

Gardez à l'esprit que la méthode erase retournera le prochain itérateur de l'itérateur passé.

À partir de ici , nous pouvons utiliser une méthode plus générée:

template<class Container, class F>
void erase_where(Container& c, F&& f)
{
    c.erase(std::remove_if(c.begin(), c.end(),std::forward<F>(f)),
            c.end());
}

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};
    //method 4
    auto is_odd = [](int x){return x % 2;};
    erase_where(vecInt, is_odd);

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;    
}

Voir ici pour savoir comment utiliser std::remove_if. https://fr.cppreference.com/w/cpp/algorithm/remove

1
Jayhello

désolé pour le nécropostage et désolé si mon expertise en c ++ m'empêche de répondre, mais si vous essayez de parcourir chaque élément et d'effectuer des modifications (comme l'effacement d'un index), essayez d'utiliser un backwords pour loop. 

for(int x=vector.getsize(); x>0; x--){

//do stuff
//erase index x

}

lors de l'effacement de l'index x, la prochaine boucle sera pour l'élément "devant" la dernière itération. J'espère vraiment que cela a aidé quelqu'un

1
lilbigwill99

OK, je suis en retard, mais de toute façon: désolé, je ne corrige pas ce que j'ai lu jusqu'à présent - c'est est possible, vous avez juste besoin de deux itérateurs:

std::vector<IInventory*>::iterator current = inv.begin();
for (IInventory* index : inv)
{
    if(/* ... */)
    {
        delete index;
    }
    else
    {
        *current++ = index;
    }
}
inv.erase(current, inv.end());

Le simple fait de modifier la valeur indiquée par un itérateur n'invalide aucun autre itérateur, ce qui permet de le faire sans s'inquiéter. En fait, std::remove_if (au moins implémentation de gcc) fait quelque chose de très similaire (en utilisant une boucle classique ...), ne supprime rien et n’efface pas.

Sachez cependant que cela n’est pas thread-safe (!) - mais cela s’applique également à certaines des solutions ci-dessus ...

1
Aconcagua

Une solution beaucoup plus élégante serait de passer à std::list (en supposant que vous n'avez pas besoin d'un accès rapide aléatoire).

list<Widget*> widgets ; // create and use this..

Vous pouvez ensuite supprimer avec .remove_if et un foncteur C++ en une seule ligne:

widgets.remove_if( []( Widget*w ){ return w->isExpired() ; } ) ;

Donc, je suis en train d’écrire un foncteur qui accepte un argument (le Widget*). La valeur de retour est la condition à laquelle supprimer un Widget* de la liste.

Je trouve cette syntaxe acceptable. Je ne pense pas que j'utiliserais jamais remove_if pour std :: vectors - il y a tant de bruit inv.begin() et inv.end() là-bas, il vaut probablement mieux utiliser une suppression basée sur un index entier ou simplement une suppression simple et ancienne basée sur un itérateur (comme indiqué ci-dessous). Mais vous ne devriez pas vraiment supprimer beaucoup du std::vector de toute façon, il est donc conseillé de passer à list pour ce cas de suppression fréquente au milieu de la liste.

Notez cependant que je n'ai pas eu la possibilité d'appeler delete sur les Widget* qui ont été supprimés. Pour ce faire, cela ressemblerait à ceci:

widgets.remove_if( []( Widget*w ){
  bool exp = w->isExpired() ;
  if( exp )  delete w ;       // delete the widget if it was expired
  return exp ;                // remove from widgets list if it was expired
} ) ;

Vous pouvez également utiliser une boucle régulière basée sur un itérateur, comme ceci:

//                                                              NO INCREMENT v
for( list<Widget*>::iterator iter = widgets.begin() ; iter != widgets.end() ; )
{
  if( (*iter)->isExpired() )
  {
    delete( *iter ) ;
    iter = widgets.erase( iter ) ; // _advances_ iter, so this loop is not infinite
  }
  else
    ++iter ;
}

Si vous n'aimez pas la longueur de for( list<Widget*>::iterator iter = widgets.begin() ; ..., vous pouvez utiliser

for( auto iter = widgets.begin() ; ...
0
bobobobo

vous ne pouvez pas supprimer l'itérateur pendant l'itération de la boucle, car le nombre d'itérateurs est incompatible et après une certaine itération, vous obtenez un itérateur non valide.

Solution: 1) prenez la copie du vecteur original 2) parcourez l'itérateur à l'aide de cette copie 2) effectuez quelques opérations et supprimez-le du vecteur d'origine.

std::vector<IInventory*> inv;
inv.Push_back(new Foo());
inv.Push_back(new Bar());

std::vector<IInventory*> copyinv = inv;
iteratorCout = 0;
for (IInventory* index : copyinv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    inv.erase(inv.begin() + iteratorCout);
    iteratorCout++;
}  
0
suraj kumar

Je pense que je ferais ce qui suit ...

for (auto itr = inv.begin(); itr != inv.end();)
{
   // Do some stuff
   if (OK, I decided I need to remove this object from 'inv')
      itr = inv.erase(itr);
   else
      ++itr;
}
0
ajpieri

En opposition à ce titre de fils, j'utiliserais deux passes:

#include <algorithm>
#include <vector>

std::vector<IInventory*> inv;
inv.Push_back(new Foo());
inv.Push_back(new Bar());

std::vector<IInventory*> toDelete;

for (IInventory* index : inv)
{
    // Do some stuff
    if (deleteConditionTrue)
    {
        toDelete.Push_back(index);
    }
}

for (IInventory* index : toDelete)
{
    inv.erase(std::remove(inv.begin(), inv.end(), index), inv.end());
}
0
nikc