Видалення елемента з вектора, перебуваючи в циклі C ++ 11 діапазон 'for'?


97

У мене є вектор 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: Це все одно зіпсує ітерацію.
Ben Voigt

@TomJ, і це було б вбивцею продуктивності - при кожному стиранні ви переміщуєте всі елементи після стираного.
Іван

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

Відповіді:


94

Ні, ти не можеш. Діапазон залежить 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краще.
Бен Войгт

6
ця відповідь є правильним, eraseповертає новий дійсний итератор. це може бути не ефективно, але гарантовано спрацює.
sp2danny

56

Кожного разу, коли елемент вилучається з вектора, ви повинні вважати, що ітератори в стираному елементі або після нього втрачають силу, оскільки кожен із наступних елементів стирається.

For-loop на основі діапазону - це просто синтаксичний цукор для "звичайного" циклу з використанням ітераторів, тому вищезазначене застосовується.

З огляду на це, ви можете просто:

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

5
" тому що існує ймовірність, що вектор перерозподілив блок пам'яті, в якому він зберігає свої елементи " Ні, a vectorніколи не перерозподіляється через виклик до erase. Причина, через яку ітератори втрачають силу, полягає в тому, що кожен із елементів, наступних за стираним елементом, переміщується.
ildjarn

2
Захоплення за замовчуванням [&]було б доречним, щоб дозволити йому "робити щось" із локальними змінними.
Potatoswatter

2
Це не виглядає простіше , ніж петлі ітератора на основі, крім того , ви повинні пам'ятати , щоб оточити remove_ifз .erase, в іншому випадку нічого не відбувається.
bobobobo

2
@bobobobo Якщо під "циклом на основі ітератора" ви маєте на увазі відповідь Сет Карнегі , то це в середньому O (n ^ 2). std::remove_ifдорівнює O (n).
Бранко Димитрієвич

У вас є справді хороший момент, помінявши елементи в зворотному боці списку та уникаючи фактично переміщення елементів до тих пір, поки не буде виконано всі еліти, які потрібно «видалити» (що remove_ifпотрібно робити всередині). Однак якщо у вас є vector5 елементів, а ви лише .erase() 1 за раз, це не впливає на продуктивність використання ітераторів проти remove_if. Якщо список більший, вам дійсно слід перейти туди, std::listде є багато видалення з середини списку.
bobobobo

16

Ви в ідеалі не повинні модифікувати вектор під час ітерації над ним. Використовуйте ідіому видалення стирання. Якщо ви це зробите, ви, ймовірно, зіткнетеся з кількома проблемами. Оскільки в a vectorробить eraseнедійсними всі ітератори, що починаються зі стирання елемента до, end()вам потрібно буде переконатися, що ваші ітератори залишаються дійсними, використовуючи:

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

Отже, тут я просто пишу функтор, який приймає один аргумент (the Widget*). Повернене значення - це умова, при якій слід видалити aWidget* зі списку.

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

Однак зверніть увагу, що я не мав можливості зателефонувати 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) для обох контейнерів.
Бен Войгт

2
Попереднє маркування не потрібно; це цілком можливо зробити за один прохід. Вам просто потрібно відстежувати "наступний елемент, який перевіряється", і "наступний слот, який потрібно заповнити". Подумайте про це як про створення копії списку, відфільтрованого на основі предиката. Якщо предикат повертає true, пропустіть елемент, інакше скопіюйте його. Але копія списку робиться на місці, а замість копіювання використовується обмін / переміщення.
Бен Войгт

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.