如何在迭代时从地图中删除?

Posted

技术标签:

【中文标题】如何在迭代时从地图中删除?【英文标题】:How to remove from a map while iterating it? 【发布时间】:2012-01-04 07:23:01 【问题描述】:

喜欢:

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

如果我使用map.erase,它将使迭代器无效

【问题讨论】:

类似:***.com/questions/1038708/… 更相似:***.com/questions/800955/… 还有:***.com/questions/180516/… 同题:***.com/questions/263945/… 【参考方案1】:

标准的关联容器擦除习语:

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-iterators。在那里你不得不说:

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

从容器中删除元素与元素的常量性并不矛盾。以此类推,delete p 始终是完全合法的,其中p 是指向常量的指针。常量不限制生命周期; C++ 中的 const 值仍然可以停止存在。

【讨论】:

“甚至没有暴露循环体内的容器”你是什么意思? @Dani:嗯,将其与 20 世纪的建筑 for (int i = 0; i &lt; v.size(); i++) 进行对比。这里我们必须在循环中说v[i],即我们必须明确提及容器。另一方面,RBFL 引入了可直接用作值的循环变量,因此循环内部不需要容器知识。这是 RBFL 用于不必了解容器的循环的预期用途的线索。擦除是完全相反的情况,它与容器有关。 @skyhisi:确实。这是后增量的合法用途之一:First 增加it 以获得下一个有效的迭代器,然后然后 擦除旧的迭代器。反过来就不行了! 我在某处读到,在 C++11 中,it = v.erase(it); 现在也适用于地图。也就是说,all 关联元素上的 erase() 现在返回下一个迭代器。因此,不再需要在 delete() 中需要后增量 ++ 的旧 kludge。这(如果为真)是一件好事,因为 kludge 依赖于函数调用内的覆盖后增量魔术,由新手维护人员“修复”以从函数调用中取出增量,或交换它到一个预增量“因为这只是一种风格”,等等。 您为什么要在if else 块中调用it++这些之后调用一次还不够吗?【参考方案2】:

很伤心,嗯?我通常这样做的方式是构建一个迭代器容器,而不是在遍历期间删除。然后循环遍历容器并使用 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)

【讨论】:

但是删除一个之后剩下的就失效了 不:***.com/questions/263945/… @Dani:不在地图中。地图中的擦除只会使被擦除项目的迭代器无效。【参考方案3】:

简而言之“如何在迭代时从地图中删除?”

使用旧地图 impl:你不能 使用新地图 impl:几乎就像 @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.

【讨论】:

我不明白。 mi.erase(it++); 有什么问题? @lvella 参见操作。 “如果我使用 map.erase,它将使迭代器无效”。 如果擦除后地图变空,您的新方法将不起作用。在这种情况下,迭代器将失效。所以,擦除后不久,最好插入if(mi.empty()) break;【参考方案4】:

我个人更喜欢这种更清晰、更简单的模式,但代价是额外的变量:

for (auto it = m.cbegin(), next_it = it; it != m.cend(); it = next_it)

  ++next_it;
  if (must_delete)
  
    m.erase(it);
  

这种方法的优点:

for 循环增量器作为增量器是有意义的; 擦除操作是简单的擦除,而不是与增量逻辑混合; 在循环体的第一行之后,itnext_it 的含义在整个迭代过程中保持不变,允许您轻松添加引用它们的附加语句,而不必担心它们是否会按预期工作(除了当然,删除后你不能使用it)。

【讨论】:

我实际上可以想到另一个优点,如果循环调用代码来删除正在迭代的条目或以前的条目(并且循环不知道它),它将工作而不会造成任何伤害。唯一的限制是某些东西是否正在擦除 next_it 或后继者所指向的内容。也可以测试完全清除的列表/地图。 这个答案简单明了,即使循环更复杂并且有多个层次的逻辑来决定是否删除,或者执行其他各种任务。不过,我建议进行编辑以使其更简单一些。 "next_it" 可以在 for 的 init 中设置为 "it" 以避免拼写错误,并且由于 init 和迭代语句都将它和 next_it 设置为相同的值,因此您不需要说 "next_it = it;"在循环的开始。 请记住任何使用此答案的人:您必须在 for 循环中包含“++next_it”,而不是在迭代表达式中。如果您尝试将其作为“it = next_it++”移动到迭代表达式中,那么在最后一次迭代中,当“it”将设置为等于“m.cend()”时,您将尝试迭代“next_it”过去的“m.cend()”,这是错误的。 我认为所有的优点都是缺点。【参考方案5】:

假设 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) 更易于阅读/维护。

【讨论】:

【参考方案6】:

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

【讨论】:

以上是关于如何在迭代时从地图中删除?的主要内容,如果未能解决你的问题,请参考以下文章

如何在迭代字典时从字典中删除项目?

如何在迭代时从 HashMap 中删除键? [复制]

Python:如何在迭代列表时从列表中删除元素而不跳过未来的迭代

迭代Java时从数组中删除对象

Qt在迭代时从QMultiHash中删除项目

迭代时从 HashSet 中删除元素 [重复]