2011-04-15 92 views
12

我找不到如何做到这一点的实例,所以我希望有人能帮助我。我有一个在类中定义的地图如下:带删除的C++映射迭代

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 /钯错误的移除事件代码。现在修复。

回答

4

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

但是它可能会调用一个函数 - > 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。已经访问过的标有要删除的元素将在下一个步骤中清除。

6

一般来说,在迭代过程中修改集合是不被赞同的。当集合被修改时,许多集合都会使迭代器失效,其中包括C#中的许多容器(我知道你在使用C++)。您可以创建一个在迭代过程中想要移除的事件向量,然后将其移除。

+0

C#的集合类在这方面很糟糕。如果他们被修改,他们将使所有*失效。 C++容器在这方面的表现通常要好得多,并且通常只会使特定的迭代器无效,因此您可以在迭代时删除 – jalf 2011-04-15 06:42:05

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并在此之后立即增加迭代器,然后返回拷贝值,这意味着迭代器在删除操作之前增加,这对您的需求应该是安全的。

+0

在迭代过程中擦除是一种常见模式,回调通常使用返回值来实现,该返回值确定需要移除:'if(it-> second(this)){translationEvents.erase(it ++); } else ++ it',其中每个回调将在这个执行后返回'true'(如果它有效)。 – 2011-04-15 07:55:59

1

问题是++it遵循可能的删除。这对你有用吗?

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

这个问题表明,可以删除除当前元素之外的其他元素。如果下一个元素被删除,这不会真的起作用。 – Anton 2011-04-15 03:13:30

3

我的解决方案是首先创建一个临时容器,并与原来的容器交换。然后,您可以通过临时容器进行迭代,然后将想要保留的容器插入原始容器。如果不想被依然古色古香,并返回false即可去除

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函数应返回true。

bool BaseSprite::RemoveTranslationEvent(const std::string &index) 
{ 
    bool keep = false; 
    return keep; 
} 
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; 
     } 
    } 
} 

一个明显的警告是,如果一个事件被迭代,然后被删除,它将不会有机会在当前调度循环期间被再次迭代以被删除。

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

+0

+1,这个想法可以通过使用第二个容器(通过引用传递给回调函数)进行扩展,如果需要删除它们,他们会在其中插入一个迭代器给自己(或其他迭代器)。执行循环完成后,您可以迭代候选列表并将它们逐出。回调的实际接口可以保持不变,方法是在保存容器的对象上添加一个'markForDeletion'方法。 – 2011-04-15 07:59:59

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 
} 

否则(如果该功能可能删除任何元素),它可能会更复杂一点。