Удаление элемента из вектора, находясь в диапазоне С ++ 11 для цикла?


98

У меня есть вектор IInventory *, и я просматриваю список, используя диапазон C ++ 11 для работы с каждым из них.

Проделав кое-что с одним, я могу удалить его из списка и удалить объект. Я знаю, что могу вызвать deleteуказатель в любое время, чтобы очистить его, но как правильно удалить его из вектора, находясь в forцикле диапазона ? И если я удалю его из списка, мой цикл станет недействительным?

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'...
}

8
Если вы хотите пофантазировать, вы можете использовать std::remove_ifс предикатом, который «делает что-то», а затем возвращает true, если вы хотите удалить элемент.
Бенджамин Линдли

Есть ли причина, по которой вы не можете просто добавить счетчик индекса, а затем использовать что-то вроде inv.erase (index)?
TomJ

4
@TomJ: Это все равно испортит итерацию.
Бен Фойгт

@TomJ, и это будет убийца производительности - при каждом стирании вы можете перемещать все элементы после стертого.
Иван

1
@BenVoigt Я рекомендовал перейти на std::listниже
bobobobo

Ответы:


95

Нет, не можешь. На основе диапазона forиспользуется, когда вам нужно получить доступ к каждому элементу контейнера один раз.

Вы должны использовать обычный forцикл или один из его родственников, если вам нужно изменить контейнер по мере продвижения, получить доступ к элементу более одного раза или иным образом выполнить итерацию нелинейным образом через контейнер.

Например:

auto i = std::begin(inv);

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

5
Разве здесь не применима идиома «стереть-удалить»?
Naveen

4
@Naveen Я решил не пытаться этого делать, потому что, очевидно, ему нужно перебирать каждый элемент, производить с ним вычисления, а затем, возможно, удалять его из контейнера. Erase-remove говорит о том, что вы просто стираете элементы, для которых возвращается предикат true, AFAIU, и кажется, что таким способом лучше не смешивать логику итераций с предикатом.
Сет Карнеги

4
@SethCarnegie Erase-remove с лямбдой для предиката элегантно позволяет это (поскольку это уже C ++ 11)
Potatoswatter

11
Не нравится это решение, для большинства контейнеров это O (N ^ 2). remove_ifлучше.
Ben Voigt

6
этот ответ является правильным, eraseвозвращает новый действительный итератор. это может быть неэффективно, но гарантированно работает.
sp2danny 01

56

Каждый раз, когда элемент удаляется из вектора, вы должны предполагать, что итераторы на удаленном элементе или после него больше не действительны, потому что каждый из элементов, следующих за удаленным элементом, перемещается.

Цикл for на основе диапазона - это просто синтаксический сахар для «нормального» цикла, использующего итераторы, поэтому применимо вышеизложенное.

При этом вы можете просто:

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

6
« Потому что есть вероятность того, что вектор перераспределен блок памяти , в которой он держит свои элементы » Нет, vectorникогда не перераспределение в связи с вызовом erase. Причина, по которой итераторы становятся недействительными, состоит в том, что перемещаются все элементы, следующие за удаленным элементом.
ildjarn

2
Захват-умолчанию было [&]бы целесообразно, чтобы позволить ему «делать некоторые вещи» с локальными переменными.
Potatoswatter

2
Это не выглядит проще , чем петли итератора на основе, кроме того , вы должны помнить , чтобы окружить remove_ifс .erase, в противном случае ничего не происходит.
bobobobo

3
@bobobobo Если под «циклом на основе итератора» вы имеете в виду ответ Сета Карнеги , то в среднем это O (n ^ 2). std::remove_ifравно O (n).
Бранко Димитриевич

1
@bobobobo Если вам действительно не нужен произвольный доступ.
Бранко Димитриевич

16

В идеале вы не должны изменять вектор во время итерации по нему. Используйте идиому «стереть-удалить». Если вы это сделаете, вы, вероятно, столкнетесь с несколькими проблемами. Поскольку в недействительными все итераторы , начиная с элемента стирается до величин вам нужно будет убедиться , что ваши итераторы остаются действительными с помощью:vectoreraseend()

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

Обратите внимание, что вам нужен b != v.end()тест «как есть». Если вы попытаетесь оптимизировать его следующим образом:

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

вы столкнетесь с UB, так как ваш eстановится недействительным после первого eraseвызова.


@ildjarn: Ага, правда! Это была опечатка.
dirkgently

2
Это не идиома «стереть-удалить». Нет вызова std::remove, и это O (N ^ 2), а не O (N).
Potatoswatter

1
@Potatoswatter: Конечно, нет. Я пытался указать на проблемы с удалением при повторении. Полагаю, моя формулировка не прошла проверку?
dirkgently

5

Строгое ли требование удалять элементы в этом цикле? В противном случае вы можете установить указатели, которые хотите удалить, на NULL и сделать еще один проход по вектору, чтобы удалить все указатели 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);

1

извините за некропостинг и также извините, если мой опыт в C ++ мешает моему ответу, но если вы пытаетесь перебрать каждый элемент и внести возможные изменения (например, стереть индекс), попробуйте использовать обратные слова для цикла.

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

//do stuff
//erase index x

}

при стирании индекса x следующий цикл будет для элемента «перед» последней итерацией. я очень надеюсь, что это помогло кому-то


просто не забывайте, когда вы используете x для доступа к определенному индексу, делайте x-1 lol
lilbigwill99

1

Хорошо, я опоздала, но все равно: К сожалению, не правильно , что я читал до сих пор - это это возможно, вам просто нужно два итератора:

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

Простое изменение значения, на которое указывает итератор, не делает недействительным любой другой итератор, поэтому мы можем сделать это, не беспокоясь. На самом деле std::remove_if(по крайней мере, реализация gcc) делает что-то очень похожее (используя классический цикл ...), просто ничего не удаляет и не стирает.

Однако имейте в виду, что это не потокобезопасный (!) - однако это также относится и к некоторым другим решениям, указанным выше ...


Какого черта. Это перебор.
Kesse

@Kesse Правда? Это наиболее эффективный алгоритм, который вы можете получить с векторами (не имеет значения, цикл на основе диапазона или классический цикл итератора): таким образом вы перемещаете каждый элемент в векторе не более одного раза, и вы выполняете итерацию по всему вектору ровно один раз. Сколько раз вы бы перемещали последующие элементы и перебирали вектор, если бы вы удалили каждый соответствующий элемент с помощью erase(при условии, конечно, что вы удалите более одного элемента)?
Аконкагуа,

1

Я покажу на примере ниже пример удаления нечетных элементов из вектора:

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;

}

вывод aw ниже:

024
024
024

Имейте в виду, что метод eraseвернет следующий итератор переданного итератора.

Из здесь , мы можем использовать более генерировать метод:

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

См. Здесь, чтобы узнать, как использовать std::remove_if. https://en.cppreference.com/w/cpp/algorithm/remove


1

В противовес этому заголовку темы я бы использовал два прохода:

#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

Гораздо более элегантным решением было бы переключиться на std::list(при условии, что вам не нужен быстрый произвольный доступ).

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

Затем вы можете удалить с помощью .remove_ifи функтор C ++ в одной строке:

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

Итак, здесь я просто пишу функтор, который принимает один аргумент ( Widget*). Возвращаемое значение - это условие, при котором необходимо удалить Widget*из списка.

Я считаю этот синтаксис приемлемым. Я не думаю , что я когда - либо использовать remove_ifдля STD :: векторов - есть так много inv.begin()и inv.end()шум там вы , вероятно , лучше использовать целый индекс на основе удаление или просто обычные старые регулярный итератор на основе удаления (как показано ниже). Но в std::vectorлюбом случае вам не следует удалять из середины очень много, поэтому рекомендуется переключаться на a listв этом случае частого удаления середины списка.

Обратите внимание, однако, у меня не было возможности позвонить deleteв Widget*удаленные. Для этого это будет выглядеть так:

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

Вы также можете использовать обычный цикл на основе итератора, например:

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

Если вам не нравится длина for( list<Widget*>::iterator iter = widgets.begin() ; ..., вы можете использовать

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

1
Я не думаю, что вы понимаете, как remove_ifна std::vectorсамом деле работает, и как он сохраняет сложность до O (N).
Бен Фойгт

Это не имеет значения. Удаление из середины std::vectorвсегда будет сдвигать каждый элемент после того, который вы удалили, на один вверх, что делает std::listгораздо лучший выбор.
bobobobo

1
Нет, он не будет «сдвигать каждый элемент вверх на один». remove_ifсдвинет каждый элемент вверх на количество освобожденных пробелов. К тому времени , ваша учетная запись для использования кэша, remove_ifна std::vectorвероятного удаления обгоняет от а std::list. И сохраняет O(1)произвольный доступ.
Бен Фойгт,

1
Тогда у вас есть отличный ответ в поисках вопроса. В этом вопросе говорится об итерации списка, который равен O (N) для обоих контейнеров. И удаление элементов O (N), что также является O (N) для обоих контейнеров.
Ben Voigt

2
Предварительная маркировка не требуется; это вполне возможно сделать за один проход. Вам просто нужно отслеживать «следующий элемент для проверки» и «следующий слот для заполнения». Думайте об этом как о создании копии списка, отфильтрованной на основе предиката. Если предикат возвращает истину, пропустите элемент, в противном случае скопируйте его. Но копия списка делается на месте, и вместо копирования используется замена / перемещение.
Бен Фойгт

0

Думаю, я бы сделал следующее ...

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

вы не можете удалить итератор во время итерации цикла, потому что количество итераторов не совпадает, и после некоторой итерации у вас будет недопустимый итератор.

Решение: 1) возьмите копию исходного вектора 2) повторите итератор, используя эту копию 2) сделайте что-нибудь и удалите его из исходного вектора.

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

Стирание элементов по одному легко приводит к производительности N ^ 2. Лучше отметить элементы, которые нужно стереть, и стереть их сразу после цикла. Если я могу предположить nullptr в недопустимом элементе в вашем векторе, тогда

std::vector<IInventory*> inv;
// ... push some elements to inv
for (IInventory*& index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    {
      delete index;
      index =nullptr;
    }
}
inv.erase( std::remove( begin( inv ), end( inv ), nullptr ), end( inv ) ); 

должно сработать.

В случае, если ваш «Сделай что-нибудь» не изменяет элементы вектора и используется только для принятия решения об удалении или сохранении элемента, вы можете преобразовать его в лямбда (как было предложено в чьей-то более ранней публикации) и использовать

inv.erase( std::remove_if( begin( inv ), end( inv ), []( Inventory* i )
  {
    // DO some stuff
    return OK, I decided I need to remove this object from 'inv'...
  } ), end( inv ) );
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.