【问题标题】:How to efficiently replace elements in an unordered_set while iterating over it?如何在迭代时有效地替换 unordered_set 中的元素?
【发布时间】:2012-09-21 13:53:45
【问题描述】:

假设你有一个

std::unordered_set<std::shared_ptr<A>> as;
// (there is an std::hash<std::shared_ptr<A>> specialisation)

并且你想在迭代时替换它的一些元素:

for (auto it = as.begin(); it != as.end(); ++it) {
  if ((*it)->condition()) {
    as.erase(it);
    as.insert(std::make_shared<A>(**it));
  }
}

这可能会使eraseinsert 处的迭代器无效(如果发生重新散列),因此此循环将表现出未定义的行为,并且很可能会严重崩溃。

我能想到的一个解决方案是使用两个单独的 vectors 来缓冲 inserterase 操作,然后使用采用迭代器对进行擦除和插入的重载(这可能对重新散列更友好)。

即使我使用缓冲区方法,这仍然看起来臃肿的代码,并可能导致可能都不必要的两次重新散列。

那么,有没有更好的方法呢?

【问题讨论】:

    标签: c++ iterator replace unordered-set


    【解决方案1】:

    我只是想到了一种可能的方法(在询问之后),但也许还有更好的方法。

    将所有内容复制到向量,然后从向量重建集合应该更快:

    std::vector<std::shared_ptr> buffer;
    buffer.reserve(as.size());
    for (auto it = as.begin(); it != as.end(); ++it) {
      if ((*it)->condition()) {
        buffer.push_back(std::make_shared<A>(**it));
      } else {
        buffer.push_back(*it);
      }
    }
    as = std::unordered_set<std::shared_ptr<A>>(buffer.begin(),buffer.end());
    

    【讨论】:

    • 不要忘记assign 方法,它可以有效地将容器重置为新内容。
    • @MatthieuM.: 比operator= 有什么优势?
    • @MatthieuM.:我找不到任何关于 unordered_set::assign 的信息。你确定有这样的方法吗?
    • 有趣的是:似乎没有。 vectorlist 有一个,但似乎关联容器没有得到一个。通常优点是您不需要构建临时存储(就像您在此处所做的那样)。您始终可以通过使用 as.clear(); as.insert(buffer.begin(), buffer.end()); 来模拟它,尽管通过重用现有存储而不是释放然后重新分配节点一次一个节点,可能会更好地优化分配(在列表等中)。
    • @MatthieuM.:嗯,构造一个新对象不会比inserting 更糟,operator= 可能是常数时间,因为它会将内容从临时对象中交换出来。但我不确定何时必须使用 std::move 来允许这种行为。
    【解决方案2】:

    当您调用 as.erase(it) 时,迭代器 it 将失效。插入无序关联容器会使所有迭代器无效。因此,插入需要与迭代器分开。避免插入也是必要的,以避免处理新插入的对象:

    std::vector<std::shared_ptr<A>> replaced;
    for (auto it = as.begin(); it != as.end(); ) {
        if ((*it)->condition()) {
            replaced.push_back(std::make_shared<A>(**it));
            as.erase(it++);
        }
        else {
            ++it;
        }
    }
    std::copy(replaced.begin(), replaced.end(), std::inserter(as, as.begin());
    

    【讨论】:

    • 不,我不想这样做,因为在 unordered 集合中,即使 insert 使所有迭代器无效,正如我指出的那样在问题文本中。 erase 也使 所有 迭代器失效,而不仅仅是当前被删除的迭代器!
    • 根据 23.2.5 [unord.req] 第 13 段,它不会使除受擦除影响的迭代器以外的迭代器无效:“......擦除成员应仅使迭代器和对被擦除元素的引用无效。”但是,这意味着在同一个循环中的插入和擦除不起作用(我将从我的回复中删除它)。
    • 现在,我想到了。 std::inserter 可能会在此过程中导致多次重新散列,因此我看不到对仅导致两次重新散列顶部的解决方案的改进(请参阅 OP)。
    • 直接向后插入元素可能会导致新插入的元素再次被迭代:新元素可能会在当前迭代器位置之后结束。潜在的重新散列次数不会随着之后重新插入而改变:每个插入的对象一次潜在的重新散列。
    • 不行,看OP中代码块后面的第二段和第三段。代码块本身只是我的意图。
    【解决方案3】:

    我会将此作为对@bitmask 答案的评论。为什么不将向量用于替换元素?

    std::vector<decltype(as)::value_type> buffer;
    buffer.reserve(as.size());
    for (auto it = as.begin(); it != as.end(); )
    {
      if ((*it)->condition())
      {
        buffer.push_back(*it);
        it = as.erase(it);
      }
      else
      {
        ++it;
      }
    }
    as.insert(buffer.begin(),buffer.end());
    

    而且,如果*it 已经是shared_ptr&lt;A&gt;,我又找不到make_shared() 的理由。只需赋值并让复制构造函数/赋值运算符发挥它们的魔力。

    【讨论】:

      【解决方案4】:

      在你的情况下,我认为你可以交换:

      for(auto iter = as.begin(); iter != as.end(); ++iter)
      {
          if(/*Check deletion condition here*/)
          {
              auto newItem = std::make_shared<A>(/*...*/);
              swap(*iter, newItem);
          }
      }
      

      【讨论】:

      • 神圣僵尸!这将取消定义谋杀地图。永远不要改变元素的哈希值。永远!
      • 但是你在你的问题中是复制构造,这就是我提供交换的原因。这就是为什么我说“在你的情况下”。如果你之后要更改内部状态,它与更改密钥相同。
      • 哈希由指针值组成,因此即使我复制构造,它也将具有与前一个指针相同的哈希值。交换操作将改变元素,不允许地图将该元素放入正确的槽中; newItem 将在 *iter 的哈希下归档,这必须不同,因为 **iter 是一个旧指针,而 *newItem 是刚刚构造的。
      猜你喜欢
      • 2018-09-14
      • 2012-08-12
      • 2021-07-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多