Как удалить с карты во время итерации?

177

Как удалить с карты во время итерации? лайк:

std::map<K, V> map;
for(auto i : map)
    if(needs_removing(i))
        // remove it from the map

Если я использую map.eraseего, аннулирует итераторы

Дани
источник
Аналогично: stackoverflow.com/questions/1038708/…
Кристиан Аммер
Еще более похоже: stackoverflow.com/questions/800955/…
Kleist
И: stackoverflow.com/questions/180516/…
Кристиан Аммер,
На ту же тему: stackoverflow.com/questions/263945/…
Agostino

Ответы:

280

Стандартная ассоциативно-контейнерная идиома стирания:

for (auto it = m.cbegin(); it != m.cend() /* not hoisted */; /* no increment */)
{
  if (must_delete)
  {
    m.erase(it++);    // or "it = m.erase(it)" since C++11
  }
  else
  {
    ++it;
  }
}

Обратите внимание, что нам действительно нужен обычный forцикл, так как мы модифицируем сам контейнер. Цикл на основе диапазона должен быть строго зарезервирован для ситуаций, когда мы заботимся только об элементах. Синтаксис для RBFL проясняет это, даже не раскрывая контейнер внутри тела цикла.

Редактировать. До C ++ 11 вы не могли стереть const-итераторы. Там вы должны сказать:

for (std::map<K,V>::iterator it = m.begin(); it != m.end(); ) { /* ... */ }

Стирание элемента из контейнера не противоречит его постоянству. По аналогии, это всегда было совершенно законноdelete p где pуказатель на константу. Константность не ограничивает продолжительность жизни; константные значения в C ++ могут прекратить существование.

Керрек С.Б.
источник
1
"даже не выставляя контейнер внутри тела цикла" что ты имеешь в виду?
Дани
2
@ Дани: Ну, противопоставьте это строительству 20-го века for (int i = 0; i < v.size(); i++). Здесь мы должны сказать v[i]внутри цикла, то есть мы должны явно упомянуть контейнер. RBFL, с другой стороны, представляет переменную цикла, которая непосредственно используется в качестве значения, и поэтому знание цикла внутри цикла не требуется. Это ключ к предполагаемому использованию RBFL для циклов, которые не должны знать о контейнере. Стирание - это совершенно противоположная ситуация, когда речь идет о контейнере.
Kerrek SB
3
@skyhisi: Действительно. Это одно из законных применений постинкремента: сначала инкремент, itчтобы получить следующий, действительный итератор, а затем стереть старый. Это не работает наоборот!
Керрек С.Б.
5
Я где-то читал, что в C ++ 11 it = v.erase(it);теперь работает и для карт. То есть, erase () для всех ассоциативных элементов теперь возвращает следующий итератор. Таким образом, старый kludge, который требовал постинкремент ++ в delete (), больше не нужен. Это (если true) хорошая вещь, поскольку kludge полагался на магию переопределенного пост-инкремента в вызове функции, «исправленную» сопровождающими новичка, чтобы исключить приращение из вызова функции или обменять его до предчувствия "потому что это просто вещь стиля" и т. д.
Дьюи Морган
3
почему вы звоните it++в if и else блоки? Разве не достаточно будет позвонить один раз после этого?
Нурб
25

Я лично предпочитаю эту схему, которая немного яснее и проще за счет дополнительной переменной:

for (auto it = m.cbegin(), next_it = it; it != m.cend(); it = next_it)
{
  ++next_it;
  if (must_delete)
  {
    m.erase(it);
  }
}

Преимущества такого подхода:

  • инкрементатор цикла for имеет смысл как инкрементор;
  • операция стирания - это простое стирание, а не смешивание с логикой приращения;
  • после первой строки тела цикла значение itи next_itостается неизменным на протяжении всей итерации, что позволяет вам легко добавлять дополнительные операторы, ссылающиеся на них, без разбора на вопрос, будут ли они работать как задумано (за исключением того, что вы не можете использовать его itпосле стирания) ,
D Coetzee
источник
2
На самом деле я могу подумать о другом преимуществе: если цикл вызывает код, который стирает итеративную запись или предыдущие записи (а цикл ничего не знает об этом), он будет работать без какого-либо вреда. Единственное ограничение - стирает ли что-то, на что указывают next_it или наследники. Полностью очищенный список / карта также может быть проверена.
Ларсвад
Этот ответ прост и ясен, даже если цикл является более сложным и имеет несколько уровней логики, чтобы решить, следует ли удалять или выполнять другие различные задачи. Я предложил редактирование, чтобы сделать его немного проще. «next_it» может быть установлен на «it» в for для init, чтобы избежать опечаток, и поскольку операторы init и iteration оба устанавливают для него и next_it одинаковые значения, вам не нужно говорить «next_it = it;» в начале цикла.
cdgraham
1
Имейте в виду любого, кто использует этот ответ: у вас должен быть "++ next_it" внутри цикла for, а не в выражении итерации. Если вы попытаетесь переместить его в выражение итерации как «it = next_it ++», то на последней итерации, когда «it» будет установлено равным «m.cend ()», вы попытаетесь выполнить итерацию «next_it» мимо "m.cend ()", что является ошибочным.
cdgraham
6

Короче говоря "Как удалить с карты при ее повторении?"

  • Со старой картой подразумевается: Вы не можете
  • С новой картой подразумевается: почти как @KerrekSB предложил. Но есть некоторые синтаксические проблемы в том, что он написал.

Из карты GCC impl (примечание GXX_EXPERIMENTAL_CXX0X ):

#ifdef __GXX_EXPERIMENTAL_CXX0X__
      // _GLIBCXX_RESOLVE_LIB_DEFECTS
      // DR 130. Associative erase should return an iterator.
      /**
       *  @brief Erases an element from a %map.
       *  @param  position  An iterator pointing to the element to be erased.
       *  @return An iterator pointing to the element immediately following
       *          @a position prior to the element being erased. If no such 
       *          element exists, end() is returned.
       *
       *  This function erases an element, pointed to by the given
       *  iterator, from a %map.  Note that this function only erases
       *  the element, and that if the element is itself a pointer,
       *  the pointed-to memory is not touched in any way.  Managing
       *  the pointer is the user's responsibility.
       */
      iterator
      erase(iterator __position)
      { return _M_t.erase(__position); }
#else
      /**
       *  @brief Erases an element from a %map.
       *  @param  position  An iterator pointing to the element to be erased.
       *
       *  This function erases an element, pointed to by the given
       *  iterator, from a %map.  Note that this function only erases
       *  the element, and that if the element is itself a pointer,
       *  the pointed-to memory is not touched in any way.  Managing
       *  the pointer is the user's responsibility.
       */
      void
      erase(iterator __position)
      { _M_t.erase(__position); }
#endif

Пример со старым и новым стилем:

#include <iostream>
#include <map>
#include <vector>
#include <algorithm>

using namespace std;
typedef map<int, int> t_myMap;
typedef vector<t_myMap::key_type>  t_myVec;

int main() {

    cout << "main() ENTRY" << endl;

    t_myMap mi;
    mi.insert(t_myMap::value_type(1,1));
    mi.insert(t_myMap::value_type(2,1));
    mi.insert(t_myMap::value_type(3,1));
    mi.insert(t_myMap::value_type(4,1));
    mi.insert(t_myMap::value_type(5,1));
    mi.insert(t_myMap::value_type(6,1));

    cout << "Init" << endl;
    for(t_myMap::const_iterator i = mi.begin(); i != mi.end(); i++)
        cout << '\t' << i->first << '-' << i->second << endl;

    t_myVec markedForDeath;

    for (t_myMap::const_iterator it = mi.begin(); it != mi.end() ; it++)
        if (it->first > 2 && it->first < 5)
            markedForDeath.push_back(it->first);

    for(size_t i = 0; i < markedForDeath.size(); i++)
        // old erase, returns void...
        mi.erase(markedForDeath[i]);

    cout << "after old style erase of 3 & 4.." << endl;
    for(t_myMap::const_iterator i = mi.begin(); i != mi.end(); i++)
        cout << '\t' << i->first << '-' << i->second << endl;

    for (auto it = mi.begin(); it != mi.end(); ) {
        if (it->first == 5)
            // new erase() that returns iter..
            it = mi.erase(it);
        else
            ++it;
    }

    cout << "after new style erase of 5" << endl;
    // new cend/cbegin and lambda..
    for_each(mi.cbegin(), mi.cend(), [](t_myMap::const_reference it){cout << '\t' << it.first << '-' << it.second << endl;});

    return 0;
}

печатает:

main() ENTRY
Init
        1-1
        2-1
        3-1
        4-1
        5-1
        6-1
after old style erase of 3 & 4..
        1-1
        2-1
        5-1
        6-1
after new style erase of 5
        1-1
        2-1
        6-1

Process returned 0 (0x0)   execution time : 0.021 s
Press any key to continue.
Kashyap
источник
1
Я не понимаю В чем проблема mi.erase(it++);?
lvella
1
@ lvella см. оп. «Если я использую map.erase, это сделает недействительными итераторы».
Кашьяп
Ваш новый метод не будет работать, если после стирания карта станет пустой. В этом случае итератор будет признан недействительным. Итак, вскоре после стирания его лучше вставить if(mi.empty()) break;.
Рахат Заман
4

Черновик C ++ 20 содержит вспомогательную функцию std::erase_if.

Таким образом, вы можете использовать эту функцию, чтобы сделать это как одну строку.

std::map<K, V> map_obj;
//calls needs_removing for each element and erases it, if true was reuturned
std::erase_if(map_obj,needs_removing);
//if you need to pass only part of the key/value pair
std::erase_if(map_obj,[](auto& kv){return needs_removing(kv.first);});
PeterT
источник
3

Довольно грустно, а? Обычно я делаю это для создания контейнера итераторов вместо удаления во время обхода. Затем переберите контейнер и используйте map.erase ()

std::map<K,V> map;
std::list< std::map<K,V>::iterator > iteratorList;

for(auto i : map ){
    if ( needs_removing(i)){
        iteratorList.push_back(i);
    }
}
for(auto i : iteratorList){
    map.erase(*i)
}
Майкл Даум
источник
Но после стирания одного остальные будут недействительными
Дани
1
Нет: stackoverflow.com/questions/263945/…
Майкл Даум
@ Дани: нет на карте. Удаление на карте только делает недействительным итератор для стертого элемента.
UncleBens
3

Предполагая C ++ 11, вот тело цикла с одной строкой, если это согласуется с вашим стилем программирования:

using Map = std::map<K,V>;
Map map;

// Erase members that satisfy needs_removing(itr)
for (Map::const_iterator itr = map.cbegin() ; itr != map.cend() ; )
  itr = needs_removing(itr) ? map.erase(itr) : std::next(itr);

Пара других незначительных изменений стиля:

  • Покажите объявленный тип ( Map::const_iterator), когда это возможно / удобно, через использование auto.
  • Используйте usingдля типов шаблонов, чтобы Map::const_iteratorоблегчить чтение / обслуживание вспомогательных типов ( ).
Matt
источник