【问题标题】:C++ map iteration with deletion带删除的 C++ 映射迭代
【发布时间】:2011-08-06 00:24:06
【问题描述】:

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

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-&gt;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 错误的删除事件代码。现已修复。

【问题讨论】:

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


    【解决方案1】:

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

    • 如果您删除除当前元素之外的任何元素其他,则您是安全的,并且
    • 如果你删除当前元素,你必须先得到 next 迭代器,这样你才能继续迭代(这就是为什么大多数容器的 erase 函数返回 next 迭代器)。 std::map 没有,所以您必须手动执行此操作)

    假设您只删除了 current 元素,那么您可以像这样简单地重写循环:

    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
    }
    

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

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • C# 的集合类在这方面很糟糕。如果它们被修改,它们会使 一切 无效。 C++ 容器在这方面的表现通常要好得多,并且通常只会使特定的迭代器无效,以便您可以在迭代时删除
      【解决方案3】:

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

      但是它调用的函数是可能的->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-&gt;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&lt;bool,T&gt;,其中 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。已访问且标记为删除的元素将在下一轮中清除。

      【讨论】:

        【解决方案4】:

        我的解决方案是首先创建一个临时容器,并将其与原始容器交换。然后您可以遍历临时容器并将您想要保留的那些插入到原始容器中。

        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,如果要删除,则应返回 false。

        bool BaseSprite::RemoveTranslationEvent(const std::string &index)
        {
            bool keep = false;
            return keep;
        }
        

        【讨论】:

          【解决方案5】:

          应该有一种方法可以让您在迭代期间擦除元素,可能有点棘手。

          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。所以后缀 ++ 会先复制迭代器,然后增加迭代器,然后返回复制值,这意味着迭代器在擦除操作之前增加,这对您的要求应该是安全的。

          【讨论】:

          • 在迭代期间擦除是一种常见的模式,回调通常通过返回值来实现,该返回值确定需要删除:if ( it-&gt;second( this ) ) { translationEvents.erase(it++); } else ++it,其中每个回调将返回true,如果它在之后有效这次执行。
          【解决方案6】:

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

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

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

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

          【讨论】:

          • +1,这个想法可以通过使用第二个容器(通过引用传递给回调)来扩展,如果需要删除它们,它们会在其中向自己(或其他迭代器)插入一个迭代器。执行循环完成后,您可以遍历候选列表以移除并驱逐它们。只需向持有容器的对象添加markForDeletion 方法,回调的实际接口就可以保持不变。
          【解决方案7】:

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

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

          【讨论】:

          • 问题表明可以删除当前元素以外的某些元素。如果下一个元素被删除,这将不起作用。
          猜你喜欢
          • 1970-01-01
          • 2021-04-15
          • 2011-07-31
          • 2010-11-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多