带删除的C ++ map迭代

时间:2011-04-15 02:18:27

标签: c++ map c++11 iteration

我找不到如何做到这一点的实例,所以我希望有人可以帮助我。我在类中定义了一个映射,如下所示:

std::map<std::string, TranslationFinished> translationEvents;

TranslationFinished是一个boost :: function。我有一个方法作为我的类的一部分,遍历这个映射,调用每个函数,如:

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

然而,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;
}

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

提前致谢

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

7 个答案:

答案 0 :(得分:7)

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

  • 如果您删除任何元素其他而不是当前元素,那么您就是安全的,并且
  • 如果删除当前元素,则必须首先获取 next 迭代器,以便继续迭代(这就是大多数容器的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
}

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

答案 1 :(得分:6)

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

答案 2 :(得分:4)

在阅读了所有其他答案后,我在这里有一个优势......但是在这里。

  

然而,它可能由它调用的函数 - &gt; 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;
   }
}

在这两个选项中,我显然更喜欢[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 );
}

如果需要立即删除元素(即如果它们尚未被调用则不应在此迭代中调用它们),那么您可以通过在执行调用之前检查它是否被标记为删除来修改该方法。标记可以通过两种方式完成,其通用方法是将容器中的值类型更改为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;
}

请注意,由于回调可以删除容器中的任何元素,因此当前回调可以删除已经访问过的迭代器,因此无法删除。再说一次,你可能不在乎将空算子留一段时间,所以可以忽略它并随时执行erase。已经访问过的标记为删除的元素将在下一次传递中清除。

答案 3 :(得分:3)

我的解决方案是首先创建一个临时容器,然后将其与原始容器交换。然后,您可以通过临时容器进行迭代,并将要保留的容器插入原始容器中。

void BaseSprite::DispatchTranslationEvents()
{
    typedef std::map<std::string, TranslationFinished> container_t;

    container_t tempEvents;
    tempEvents.swap(translationEvents);

    for(auto it = tempEvents.begin(); it != tempEvents.end(); ++it)
    {
        if (true == it->second(this))
            translationEvents.insert(it);
    }
}

如果TranslationFinished函数需要保留并返回false以便删除,则bool BaseSprite::RemoveTranslationEvent(const std::string &index) { bool keep = false; return keep; } 函数应返回true。

{{1}}

答案 4 :(得分:2)

在迭代过程中应该有一种方法可以擦除元素,可能有点棘手。

for(auto it = translationEvents.begin(); it != translationEvents.end();)
{
    //remove the "erase" logic from second call
    it->second(this); 
    //do erase and increase the iterator here, NOTE: ++ action is very important
    translationEvents.erase(it++);         
}

删除元素后,迭代器将无效,因此在删除它之后,您无法再使用该迭代器来执行增加操作。但是,删除元素不会影响地图实现中的其他元素,IIRC。因此后缀++将首先复制iter并在此之后立即增加迭代器,然后返回复制值,这意味着在擦除操作之前增加迭代器,这对于您的要求应该是安全的。

答案 5 :(得分:2)

您可以将删除推迟到调度循环:

typedef boost::function< some stuff > TranslationFunc;

bool BaseSprite::RemoveTranslationEvent(const std::string &index)
{
    bool removed = false;
    auto it = translationEvents.find(index);
    if (it != translationEvents.end())
    {
        it->second = TranslationFunc(); // a null function indicates invalid event for later
        removed = true;
    }
    return removed;
}

防止在循环本身中调用无效事件,并清除所有“已删除”事件:

void BaseSprite::DispatchTranslationEvents()
{
    for(auto it = translationEvents.begin(); it != translationEvents.end();)
    {
        // here we invoke the event if it exists
        if(!it->second.empty())
        {
            it->second(this);
        }

        // if the event reset itself in the map, then we can cleanup
        if(it->second.empty())
        {
            translationEvents.erase(it++); // post increment saves hassles
        }
        else
        {
            ++it;
        }
    }
}

一个明显的警告是,如果一个事件被迭代,然后被删除,它将无法再次被迭代以在当前的调度循环中被删除。

这意味着该事件的实际删除将推迟到下次运行调度循环时。

答案 6 :(得分:1)

问题是++it跟随可能的擦除。这对你有用吗?

void BaseSprite::DispatchTranslationEvents()
{
    for(auto it = translationEvents.begin(), next = it;
        it != translationEvents.end(); it = next)
    {
        next=it;
        ++next;
        it->second(this);
    }
}