15

I want to erase some elements in my std::map.
I wrote erase + remove_if technique which I always do with other sequence containers.
But it wasn't compile with map. Why?
And How can I do this job?

std::map<int, int> m;

bool foo(const std::pair<int, int>& p)
{
    return p.second > 15;
}

int _tmain(int argc, _TCHAR* argv[])
{
    m.insert(make_pair(0, 0));
    m.insert(make_pair(1, 10));
    m.insert(make_pair(2, 20));
    m.insert(make_pair(3, 30));

    m.erase(
        remove_if(m.begin(), m.end(), foo),
        m.end()); // compile error

    return 0;
}
Benjamin
  • 10,085
  • 19
  • 80
  • 130
  • 2
    remove_if does not work for associative container. see [stackoverflow.com/q/800955/346366](http://stackoverflow.com/q/800955/346366) you'll find an equivalent – Nelstaar Aug 10 '11 at 08:30

6 Answers6

18

Write it like this for map, since remove_if won't work for map iterators (it merely puts offending elements at the end, and map iterators don't allow for this):

template <typename Map, typename F>
void map_erase_if(Map& m, F pred)
{
    typename Map::iterator i = m.begin();
    while ((i = std::find_if(i, m.end(), pred)) != m.end())
        m.erase(i++);
}

or if you like one-liners:

template <typename Map, typename F>
void map_erase_if(Map& m, F pred)
{
    for (typename Map::iterator i = m.begin();
         (i = std::find_if(i, m.end(), pred)) != m.end();
         m.erase(i++));
}
Alexandre C.
  • 55,948
  • 11
  • 128
  • 197
  • 1
    This wouldn't work, after `erase` iterators can become invalid. – Mihran Hovsepyan Aug 10 '11 at 09:23
  • 1
    @Kerrek: actually I don't like it, and I never write code like this. An empty bodied for loop is pretty much always clearer when written as a while loop. – Alexandre C. Aug 10 '11 at 11:18
  • Well, it's certainly not the most easy to read code, but it's a cool demonstration of what you can do with a `for` expression, and it makes `i` local to the loop scope without extraneous braces. Personally, I'd write `{}` at the end rather than a semicolon, I suppose. – Kerrek SB Aug 10 '11 at 11:20
9

"With other sequence containers" is your error - map is an associative container! In associative containers, elements are defined by their key (as opposed to their insertion order in sequence containers), and you erase elements by key:

m.erase(12);

Erasing by key value has the same complexity as lookup (e.g. O(log n) for map, O(1) for unordered map, etc.). Alternatively, you can erase by iterator in constant time. Erasing an iterator invalidates that iterator, but no others (again unlike in sequence containers), so if you want to iterate over a map, the typical idiom is this:

for (auto it = m.cbegin(); it != m.cend(); ) // no "++"!
{
  if (it->second > 15)  // your own condition goes here
  {
    m.erase(it++);
  }
  else
  {
    ++it;
  }
}
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
7

Because std::map is not "sequence container" :) remove_if will try to put useless elements to the end of the map, but this will cause of violating of implicit data-structure (red-black tree in most cases) of the map. The implicit data-structure defines place of each element in the map, and that is why remove_if is not allowed for std::map.

You should erase elements from std::map one after another (or giving some interval) in loop.

Somethin like this:

it = m.begin();
while ((it = std::find_if(it, m.end(), pred)) != m.end())
    m.erase(it++);
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
Mihran Hovsepyan
  • 10,810
  • 14
  • 61
  • 111
  • Erasing from a map does *not* invalidate iterators other than the erasee. – Kerrek SB Aug 10 '11 at 09:24
  • @Kerrek SB I mean that after `erase(it)` `it` becomes invalid, and `erase(it++)` should be used as in answer of @Alexandre C. – Mihran Hovsepyan Aug 10 '11 at 09:52
  • 1
    Well, but that's not what you're saying. You're saying "after erasing an element, iterators of map become invalid", which is *very* misleading. – Kerrek SB Aug 10 '11 at 10:08
  • @Alex: Oh, sorry, never mind! I overlooked that the find only starts at the last-found position. I removed the comment. Mirhan: Sorry for that, the `it++` *was* necessary. Make sure to initialize it to `begin()`, though :-) – Kerrek SB Aug 10 '11 at 11:12
  • It's has too many comparation than Kerrek SB or Nim's solution. Am I misunderstand? – Benjamin Aug 10 '11 at 11:13
  • @Benjamin: It should be the same. `find_if` stops upon its first find, so you only traverse the container once, in all the posted solutions. – Kerrek SB Aug 10 '11 at 11:15
  • @Mihran I think last edit is right code. it++ is necessary. Initializing iterator too. – Benjamin Aug 10 '11 at 11:30
  • @Mirhan: I'm sorry, your first version was right, so I edited the `++` back in so as not to have broken code, and you also need the initialization to `begin()` -- hope that's OK! :-) – Kerrek SB Aug 10 '11 at 11:31
  • @Kerrek SB yes it should be initialized, but if so then it should be defined as well `std::map::iterator it = m.begin();`. The code I post before was just part and not full program, so it was ok. – Mihran Hovsepyan Aug 10 '11 at 15:04
1

This idiom only works for sequence like containers - entries in a map (associative) cannot be re-ordered (the key doesn't change - so how you can you possibly expect to move an entry to some other position - e.g. end). The correct way to do this is to find the entry and remove it - i.e. it = map.find(); map.erase(it++)

Nim
  • 33,299
  • 2
  • 62
  • 101
  • He's not trying to remove entries by key; he's removing them based on an arbitrary function. – Nicol Bolas Aug 10 '11 at 08:31
  • @Nicol Bolas: Yes I am. But I'm reconsidering the container I should use. Why am I searching by value in std::map? -_-a Anyway, the answer is correct. Thanks guys. – Benjamin Aug 10 '11 at 08:44
0

But it wasn't compile with map. Why?

When using remove_if, the type of dereferenced iterators must meet the requirements of CopyAssignable. That is it should be possible to assign one value to another.

For std::map<int, int> the value is std::pair<const int, int> which represents key value pairs of map and which is not CopyAssignable. The reason for this const int for key is how map works internally as other people already pointed.

By the way you will get the same compilation errors for sequence container like this:

std::vector<std::pair<const int, int>>;
ks1322
  • 33,961
  • 14
  • 109
  • 164
0

Try something like this

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

class foo 
{
public:
    enum CompType { GREATER=1, LESS=-1 };
    foo(int nVal=15, enum CompType ctype=GREATER)
    : m_nVal(nVal)
    , m_bGreater(ctype==GREATER)
    {
    }
    bool operator()(std::pair<int, int> p) 
    {
        if (m_bGreater)
            return p.second > m_nVal;
        else
            return p.second < m_nVal;
    }
private:
    int  m_nVal;
    bool m_bGreater;
};

void MapRemove(std::map<int, int> &m, foo &pred)
{
    auto itr = std::find_if(m.begin(), m.end(), pred);
    while (itr != m.end())
        itr = std::find_if(m.erase(itr), m.end(), pred);
}

int main(int argc, char *argv[])
{
    std::map<int, int> m;

    m.insert(std::make_pair(0, 0));
    m.insert(std::make_pair(1, 10));
    m.insert(std::make_pair(2, 20));
    m.insert(std::make_pair(3, 30));

    MapRemove(m, foo());

    for (auto itr=m.begin(); itr!=m.end(); ++itr)
        std::cout << "(" << itr->first << ", " 
                  << itr->second << ")" << '\n';

    return 0;
}
Michael J
  • 7,631
  • 2
  • 24
  • 30