remove_if эквивалент для std :: map

118

Я пытался стереть ряд элементов с карты в зависимости от определенных условий. Как это сделать с помощью алгоритмов STL?

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

Есть ли какой-либо эквивалентный алгоритм remove_if, который работает для карты?

В качестве простого варианта я подумал о циклическом просмотре карты и стирании. Но есть ли безопасный вариант перебора карты и удаления? (Поскольку итераторы становятся недействительными после стирания)

Я использовал следующий пример:

bool predicate(const std::pair<int,std::string>& x)
{
    return x.first > 2;
}

int main(void) 
{

    std::map<int, std::string> aMap;

    aMap[2] = "two";
    aMap[3] = "three";
    aMap[4] = "four";
    aMap[5] = "five";
    aMap[6] = "six";

//      does not work, an error
//  std::remove_if(aMap.begin(), aMap.end(), predicate);

    std::map<int, std::string>::iterator iter = aMap.begin();
    std::map<int, std::string>::iterator endIter = aMap.end();

    for(; iter != endIter; ++iter)
    {
            if(Some Condition)
            {
                            // is it safe ?
                aMap.erase(iter++);
            }
    }

    return 0;
}
Aj.
источник
Что значит remove_if не работает?
dirkgently
Я не могу использовать remove_if для поиска элемента на карте, верно? Это дало ошибку времени компиляции. Я что-то упускаю?
А.Дж.
Нет - это не работает, поскольку remove_if работает, переупорядочивая последовательность, перемещая элементы, которые не соответствуют условию, ближе к концу. Следовательно, он работает с T [n], но не с отображением <T, U>.
MSalters
2
С C + 11 вы можете использовать, for(auto iter=aMap.begin(); iter!=aMap.end(); ){ ....}чтобы уменьшить беспорядок. Остальное, как говорили другие. Этот вопрос только что избавил меня от секущихся волос ;-)
Атул Кумар

Ответы:

111

Почти.

for(; iter != endIter; ) {
     if (Some Condition) {
          iter = aMap.erase(iter);
     } else {
          ++iter;
     }
}

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

Это распространенный алгоритм, который я видел и документировал во многих местах.

[EDIT] Вы правы, что итераторы становятся недействительными после стирания, но только итераторы, ссылающиеся на удаляемый элемент, другие итераторы остаются действительными. Следовательно, использование iter++в erase()вызове.

Стив Фолли
источник
4
Я запутался; почему бы вы использовали for (; ...;) вместо while (...)? Кроме того, хотя это, вероятно, работает, разве .erase не возвращает итератор следующего? Таким образом, кажется, что блог if (Some Condition) должен быть iter = aMap.erase (iter), чтобы быть наиболее совместимым. Может я чего-то упускаю? Мне не хватает опыта, который есть у некоторых из вас.
taxilian
86
Обратите внимание, что в C ++ 11 все ассоциативные контейнеры, в том числе map, возвращают следующий итератор из erase(iter). Это намного чище iter = erase( iter ).
Potatoswatter
10
@taxilian (с опозданием на несколько лет) while () или for () будет работать, но семантически люди часто используют for () для итерации по известному диапазону, а while () для неизвестного количества циклов. Поскольку в этом случае диапазон известен (от начала до endIter ), for () не будет необычным выбором и, вероятно, будет более распространенным. Но, опять же, оба варианта приемлемы.
Jamin Grey
4
@taxilian Что еще более важно: с 'for' вы можете иметь определение итератора ВНУТРИ области цикла, чтобы это не мешало остальной части вашей программы.
Sanchises
1
@athos Вопрос сформулирован пассивным тоном: "рекомендуется". Универсальной рекомендации нет. Я думаю, что мой последний комментарий - самый простой способ. Он действительно включает две копии переменной итератора, что, как кто-то здесь указал, немного теряет эффективность. Это ваш звонок, что вам подходит.
Potatoswatter 06
75

erase_if для std :: map (и других контейнеров)

Для этого я использую следующий шаблон.

namespace stuff {
  template< typename ContainerT, typename PredicateT >
  void erase_if( ContainerT& items, const PredicateT& predicate ) {
    for( auto it = items.begin(); it != items.end(); ) {
      if( predicate(*it) ) it = items.erase(it);
      else ++it;
    }
  }
}

Это ничего не вернет, но удалит элементы из std :: map.

Пример использования:

// 'container' could be a std::map
// 'item_type' is what you might store in your container
using stuff::erase_if;
erase_if(container, []( item_type& item ) {
  return /* insert appropriate test */;
});

Второй пример (позволяет передать тестовое значение):

// 'test_value' is value that you might inject into your predicate.
// 'property' is just used to provide a stand-in test
using stuff::erase_if;
int test_value = 4;  // or use whatever appropriate type and value
erase_if(container, [&test_value]( item_type& item ) {
  return item.property < test_value;  // or whatever appropriate test
});
Железный Спаситель
источник
3
@CodeAngry Спасибо - мне всегда казалось странным, что этого еще нет std. Я понимаю, почему он не является членом std::map, но я думаю, что что-то подобное должно быть в стандартной библиотеке.
Iron Savior
3
Будет добавлено в C ++ 20 дляstd::map и других.
Рой Дантон
3

Я получил эту документацию из отличного справочника SGI STL :

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

Итак, имеющийся у вас итератор, который указывает на удаляемый элемент, конечно же, будет недействителен. Сделайте что-нибудь вроде этого:

if (some condition)
{
  iterator here=iter++;
  aMap.erase(here)
}
1800 ИНФОРМАЦИЯ
источник
3
Это не отличается от исходного кода. iter ++ увеличивает итератор, затем возвращает итератор, указывающий на элемент до приращения.
Стив Фолли,
Но он не будет аннулирован, так как затем мы стираем в позиции здесь
1800 ИНФОРМАЦИЯ
@ 1800 ИНФОРМАЦИЯ: ввод вызова функции является точкой последовательности, побочный эффект приращения оценивается перед вызовом erase. Так что они действительно эквивалентны. Тем не менее, я бы предпочел вашу версию оригиналу.
peterchen
Он работает для массива или вектора, но приведет к неожиданному результату в stl-карте.
hunter_tech
2

В исходном коде есть только одна проблема:

for(; iter != endIter; ++iter)
{
    if(Some Condition)
    {
        // is it safe ?
        aMap.erase(iter++);
    }
}

Здесь iterувеличивается один раз в цикле for и еще раз в стирании, что, вероятно, закончится каким-то бесконечным циклом.

партха бисвас
источник
2

Вот какое изящное решение.

for (auto it = map.begin(); it != map.end();)
{   
    (SomeCondition) ? map.erase(it++) : (++it);
}
Корень мандрагоры
источник
1

Из нижних нот:

http://www.sgi.com/tech/stl/PairAssociativeContainer.html

ассоциативный контейнер пары не может предоставлять изменяемые итераторы (как определено в требованиях к тривиальному итератору), потому что тип значения изменяемого итератора должен быть назначаемым, а пара - не назначаемым. Однако парный ассоциативный контейнер может предоставлять итераторы, которые не являются полностью постоянными: итераторы, так что выражение (* i) .second = d является допустимым.

Piotr
источник
1

Первый

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

Во-вторых, следующий код хорош

for(; iter != endIter; )
{
    if(Some Condition)
    {
        aMap.erase(iter++);
    }
    else
    {
        ++iter;
    }
}

При вызове функции параметры оцениваются перед вызовом этой функции.

Таким образом, когда iter ++ оценивается перед вызовом удаления, оператор ++ итератора вернет текущий элемент и укажет на следующий элемент после вызова.

Винсент
источник
1

ИМХО remove_if()эквивалента нет .
Вы не можете изменить порядок карты.
Так что remove_if()нельзя ставить интересующие пары в конец, на который можно уравнять erase().

user109134
источник
Это действительно прискорбно.
allyourcode
1

На основе ответа Iron Savior Для тех, кто хотел бы предоставить более широкий спектр функциональных итераторов std.

template< typename ContainerT, class FwdIt, class Pr >
void erase_if(ContainerT& items, FwdIt it, FwdIt Last, Pr Pred) {
    for (; it != Last; ) {
        if (Pred(*it)) it = items.erase(it);
        else ++it;
    }
}

Любопытно, есть ли способ потерять ContainerTэлементы и получить их от итератора.

Грег Домьян
источник
1
«Идентификаторы, начинающиеся с подчеркивания, за которым следует заглавная буква, зарезервированы для любого использования реализацией».
СМУ
0

Я считаю, что ответ Стива Фолли более эффективен.

Вот еще одно простое, но менее эффективное решение :

Решение использует remove_copy_ifдля копирования нужных нам значений в новый контейнер, а затем меняет местами содержимое исходного контейнера с содержимым нового:

std::map<int, std::string> aMap;

...
//Temporary map to hold the unremoved elements
std::map<int, std::string> aTempMap;

//copy unremoved values from aMap to aTempMap
std::remove_copy_if(aMap.begin(), aMap.end(), 
                    inserter(aTempMap, aTempMap.end()),
                    predicate);

//Swap the contents of aMap and aTempMap
aMap.swap(aTempMap);
Aj.
источник
2
Это кажется неэффективным.
allyourcode
0

Если вы хотите стереть все элементы с ключом больше 2, то лучший способ -

map.erase(map.upper_bound(2), map.end());

Однако работает только для диапазонов, а не для любого предиката.

Тадеуш Копец
источник
0

Я использую вот так

 std::map<int, std::string> users;    
 for(auto it = users.begin(); it <= users.end()) {
    if(<condition>){
      it = users.erase(it);
    } else {
    ++it;
    }
 }
voltento
источник