[英]Deleting elements from std set while iterating leads to endless loop
以下代碼產生以下輸出,並以 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 負載。
我知道如何正確地做到這一點(見下文),但我無法理解這里發生了什么。 這不是一個無限循環,因為它應該不斷地輸出一些東西,但它只是不斷地做一些事情。 知道什么嗎?
如何以正確的方式做到這一點: 在迭代時從 std::set 中刪除元素以及如何在迭代時從 std::set 中刪除元素
Hokay,所以您問如何在不連續觸發“檢查對象”打印的情況下無限循環。
快速答案(你已經從其他人那里得到了)是在my_set.end()
上調用operator++
是 UB,因此可以做任何事情。
特別深入研究 GCC(因為 @appleapple 可以在 GCC 上重現,而我在 MSVC 中的測試沒有發現無限循環)揭示了一些有趣的東西:
operator++
調用實現為對_M_node = _Rb_tree_increment(_M_node);
的調用_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 的“有趣”部分——任何事情都有可能發生......
代碼相當於:
{
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()
,因此無限循環是可能的。
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++;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.