【问题标题】:Deleting elements from std set while iterating leads to endless loop迭代时从标准集中删除元素会导致无限循环
【发布时间】:2021-07-14 07:35:41
【问题描述】:

以下代码产生以下输出,并以 100% cpu 负载的无限循环结束。

#include <iostream>
#include <set>

class Foo{};

void delete_object_from_set(std::set<Foo *>& my_set, Foo* ob)
{
    std::set< Foo *>::iterator setIt;

    std::cout << "Try to delete object  '" << ob << "'..." << std::endl;

    for(setIt = my_set.begin(); setIt != my_set.end(); ++setIt)
    {
        Foo * tmp_ob = *setIt;
        std::cout << "Check object '" << tmp_ob << "'..." << std::endl;
        // compare the objects
        //
        if(ob == tmp_ob)
        {
            // if the objects are equal, delete this object from the set
            //
            std::cout << "Delete object '" << tmp_ob << " from set..." << std::endl;
            setIt = my_set.erase(setIt);
            std::cout << "Deleted object '" << tmp_ob << " from set..." << std::endl;
        }
    }
    std::cout << "loop finished."  << std::endl;    
};

int main()
{
  Foo* ob = new Foo();
  std::set< Foo * > my_set;
  my_set.insert(ob);

  delete_object_from_set(my_set, ob);

  std::cout << "finished" << std::endl;
  return 0;
}

输出:

Try to delete object  '0x563811ffce70'...
Check object '0x563811ffce70'...
Delete object '0x563811ffce70 from set...
Deleted object '0x563811ffce70 from set...

所以它没有完成,有 100% 的 cpu 负载。

我知道如何正确执行(见下文),但我不明白这里发生了什么。这不是一个无限循环,从那时起它应该不断地输出一些东西,但它只是一直在做something。知道什么吗?

如何正确操作:Deleting elements from std::set while iteratingHow to remove elements from an std::set while iterating over it

【问题讨论】:

  • 阅读:stackoverflow.com/questions/6438086/iterator-invalidation-rules。一旦使用它使迭代器失效,它就是未定义的行为。
  • 您正在为 end 迭代器调用 ++ - 未定义的行为。 if(ob == tmp_ob) { // ... } else ++setIt;for(setIt = my_set.begin(); setIt != my_set.end(); )
  • 谢谢,但为什么最终会导致 100% cpu 负载并且不再有输出?能说一下cpu一直在做什么吗?
  • 也许,发射火箭很可能你的 set 实现中的 operator++() 进入无限循环。如果您使用的是 gcc,您可能能够找到它的源代码(不确定其他编译器)

标签: c++


【解决方案1】:

好吧,所以你问这如何在不连续触发“检查对象”打印的情况下无限循环。

快速回答(您已经从其他人那里得到)是在 my_set.end() 上调用 operator++ 是 UB,因此可以做任何事情。

深入研究 GCC(因为@appleapple 可以在 GCC 上重现,而我在 MSVC 中的测试没有发现无限循环)揭示了一些有趣的东西:

operator++ 调用实现为对_M_node = _Rb_tree_increment(_M_node); 的调用,如下所示:

static _Rb_tree_node_base*
local_Rb_tree_increment(_Rb_tree_node_base* __x) throw ()
{
    if (__x->_M_right != 0)
        {
            __x = __x->_M_right;
            while (__x->_M_left != 0)
                __x = __x->_M_left;
        }
    else
        {
            _Rb_tree_node_base* __y = __x->_M_parent;
            while (__x == __y->_M_right)
                {
                    __x = __y;
                    __y = __y->_M_parent;
                }
            if (__x->_M_right != __y)
                __x = __y;
        }
    return __x;
}

因此,它默认通过从第一个右侧找到“下一个”节点,然后一直运行到左侧。 但是!查看my_set.end() 节点的调试器会发现以下内容:

(gdb) s
366             _M_node = _Rb_tree_increment(_M_node);
(gdb) p _M_node
$1 = (std::_Rb_tree_const_iterator<Foo*>::_Base_ptr) 0x7fffffffe2b8
(gdb) p _M_node->_M_right
$2 = (std::_Rb_tree_node_base::_Base_ptr) 0x7fffffffe2b8
(gdb) p _M_node->_M_left
$3 = (std::_Rb_tree_node_base::_Base_ptr) 0x7fffffffe2b8

end() 节点的左右两边显然都指向自身。为什么?询问实施者,但可能是因为它使其他事情更容易或更可优化。但这确实意味着在您的情况下,您遇到的 UB 本质上是一个无限循环:

__x->_M_left = __x;

while (__x->_M_left != 0)
  __x = __x->_M_left;  // __x = __x;

同样,GCC 就是这种情况,在 MSVC 上它没有循环(调试抛出异常,发布只是忽略它;完成循环并打印“循环完成”和“完成”,好像没有发生任何奇怪的事情) .但这就是关于 UB 的“有趣”部分——任何事情都有可能发生......

【讨论】:

  • 很好的答案。我从来没有想过增量运算符可能导致无限循环的可能性。有趣的是它的编译器也是特定的。谢谢!启发了我和我对c++的理解
  • @Lucker10 未定义行为未定义。
  • @Lucker10 on minGW64 它在增量时遇到调试器陷阱并为我挂在那里。如果从 Qt Creator IDE 运行,它将显示调用 INT 3 的指令。 Wandbox build 可能会执行类似的操作并自动终止进程。
【解决方案2】:

代码相当于:

{
   setIt = my_set.begin(); 
   while(setIt != my_set.end())
   {
        Foo * tmp_ob = *setIt;
        std::cout << "Check object '" << tmp_ob << "'..." << std::endl;

        if(ob == tmp_ob)
        {

            std::cout << "Delete object '" << tmp_ob << " from set..." << std::endl;
            setIt = my_set.erase(setIt);
            std::cout << "Deleted object '" << tmp_ob << " from set..." << std::endl;
        }

      ++setIt;
   }
}

如果容器的最后一个元素被删除,对my_set.erase(setIt); 的调用可能会返回end()。因此会发生增量,即 UB。是否触发异常取决于实现,但在任何后续点setIt 永远不会等于my_set.end(),因此无限循环是可能的。

【讨论】:

  • 有道理,但我不明白为什么cout 没有继续输出
【解决方案3】:
for(setIt = my_set.begin(); setIt != my_set.end();)
{
    Foo * tmp_ob = *setIt;
    std::cout << "Check object '" << tmp_ob << "'..." << std::endl;
    // compare the objects
    //
    if(ob == tmp_ob)
    {
        // if the objects are equal, delete this object from the set
        //
        std::cout << "Delete object '" << tmp_ob << " from set..." << std::endl;
        setIt = my_set.erase(setIt);
        std::cout << "Deleted object '" << tmp_ob << " from set..." << std::endl;
    }
    else
        setIt++;

}

【讨论】:

  • 我看不出这如何回答 OP 问题,OP 知道代码使用了无效的迭代器
  • 这如何回答这个问题??
  • my_set.erase(setIt) 返回被擦除的迭代器旁边的迭代器,如果这个迭代器是迭代的结束,则循环结束 setIt++ 将导致无限循环。
猜你喜欢
  • 2017-04-25
  • 1970-01-01
  • 1970-01-01
  • 2012-05-13
  • 2017-05-02
相关资源
最近更新 更多