带删除的C++ map迭代

Meg*_*ron 13 c++ iteration map c++11

我找不到如何做到这一点的实例,所以我希望有人可以帮助我.我在类中定义了如下地图:

std::map<std::string, TranslationFinished> translationEvents;
Run Code Online (Sandbox Code Playgroud)

TranslationFinished是一个boost :: function.我有一个方法作为我的类的一部分,迭代遍历此映射,调用每个函数,如下所示:

void BaseSprite::DispatchTranslationEvents()
{
    for(auto it = translationEvents.begin(); it != translationEvents.end(); ++it)
    {
        it->second(this);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,被调用的函数可以it->second(this);使用以下函数从translationEvents映射(通常是自身)中删除元素:

bool BaseSprite::RemoveTranslationEvent(const std::string &index)
{
    bool removed = false;
    auto it = translationEvents.find(index);
    if (it != translationEvents.end())
    {
        translationEvents.erase(it);
        removed = true;
    }
    return removed;
}
Run Code Online (Sandbox Code Playgroud)

执行此操作会导致调试断言在DispatchTranslationEvents()尝试递增迭代器时失败.有没有办法安全地遍历地图,迭代期间函数调用可能会从地图中删除元素?

提前致谢

编辑:意外地C/Pd错误的删除事件代码.现在修复了.

jal*_*alf 7

map::erase使显示的删除迭代器(显然)无效,但不会使地图的其余部分无效.这意味着:

  • 如果你删除的任何元素其他比当前,你是安全的,并
  • 如果删除当前元素,则必须先获取下一个迭代器,这样就可以继续迭代(这就是erase大多数容器的函数返回下一个迭代器的原因).std::map没有,所以你必须手动这样做)

假设你只删除当前元素,那么你可以简单地重写这样的循环:

for(auto it = translationEvents.begin(); it != translationEvents.end();)
{
    auto next = it;
    ++next; // get the next element
    it->second(this); // process (and maybe delete) the current element
    it = next; // skip to the next element
}
Run Code Online (Sandbox Code Playgroud)

否则(如果函数可能删除任何元素),它可能会变得更复杂.


Ste*_*eve 6

一般来说,在迭代期间修改集合是不受欢迎的.修改集合时,许多集合使迭代器无效,包括C#中的许多容器(我知道你使用的是C++).您可以在迭代期间创建要删除的事件向量,然后将其删除.


Dav*_*eas 4

阅读完所有其他答案后,我在这里处于优势......但事情就这样了。

然而, it->second(this); 调用的函数是可能的。从 TranslationEvents 映射中删除一个元素(通常是它本身)

如果这是真的,即回调可以从容器中删除任何元素,则您不可能从循环本身解决此问题。

删除当前回调

在回调只能删除自身的更简单的情况下,您可以使用不同的方法:

// [1] Let the callback actually remove itself
for ( iterator it = next = m.begin(); it != m.end(); it = next ) {
   ++next;
   it->second(this);
}
// [2] Have the callback tell us whether we should remove it
for ( iterator it = m.begin(); it != m.end(); ) {
   if ( !it->second(this) ) {                   // false means "remove me"
      m.erase( it++ );
   } else {
      ++it;
   }
}
Run Code Online (Sandbox Code Playgroud)

在这两个选项中,我显然更喜欢 [2],因为您将回调与处理程序的实现解耦。也就是说,[2] 中的回调根本不知道它所在的容器。[1] 具有更高的耦合性(回调了解容器)并且更难以推理,因为容器是从代码中的多个位置更改的。一段时间后,您甚至可能会回顾代码,认为这是一个奇怪的循环(不记得回调会自行删除)并将其重构为更明智的内容,例如for ( auto it = m.begin(), end = m.end(); it != end; ++it ) it->second(this);

删除其他回调

对于可以删除任何其他回调的更复杂的问题,这完全取决于您可以做出的妥协。在简单的情况下,它仅在完成迭代删除其他回调,您可以提供一个单独的成员函数来保留要删除的元素,然后在循环完成后立即将它们全部删除:

void removeElement( std::string const & name ) {
   to_remove.push_back(name);
}
...
for ( iterator it = m.begin(); it != m.end(); ++it ) {
   it->second( this );       // callback will possibly add the element to remove
}
// actually remove
for ( auto it = to_remove.begin(); it != to_begin.end(); ++it ) {
   m.erase( *it );
}
Run Code Online (Sandbox Code Playgroud)

如果需要立即删除元素(即,如果尚未调用它们,即使在本次迭代中也不应该调用它们),那么您可以通过在执行调用之前检查它是否被标记为删除来修改该方法。标记可以通过两种方式完成,一般是将容器中的值类型更改为 a pair<bool,T>,其中 bool 指示它是否处于活动状态。如果像在这种情况下一样,所包含的对象可以更改,那么您可以这样做:

void removeElement( std::string const & name ) {
   auto it = m.find( name );           // add error checking...
   it->second = TranslationFinished(); // empty functor
}
...
for ( auto it = m.begin(); it != m.end(); ++it ) {
   if ( !it->second.empty() )
      it->second(this);
}
for ( auto it = m.begin(); it != m.end(); ) { // [3]
   if ( it->second.empty() )
      m.erase( it++ );
   else
      ++it;
}
Run Code Online (Sandbox Code Playgroud)

请注意,由于回调可以删除容器中的任何元素,因此您不能随时删除,因为当前回调可能会删除已访问过的迭代器。话又说回来,您可能不介意暂时保留空函子,因此忽略它并erase随心所欲地执行可能就可以了。已访问且标记为删除的元素将在下一次遍历中被清除。