迭代unordered_set时元素可能被删除的优雅解决方案
我遇到的问题最小复现版本如下:我有一个存储对象指针的unordered_set,在迭代该集合的过程中,部分对象需要被从集合中删除。在大型程序中这会导致崩溃,而在此示例中仅删除一个元素后循环就终止了。
场景是一款包含Unit的游戏:闲置的Unit会被触发执行动作,闲置Unit被记录在名为idle_units的unordered_set中。在游戏的更新循环中,程序会遍历idle_units并让其中的Unit执行act()方法,调用act()后该Unit不再处于闲置状态,会被从idle_units中移除。
复现代码
#include <vector> #include <memory> #include <unordered_set> #include <unordered_map> #include <cstdlib> #include <iostream> // forward declarations struct Unit; void set_not_idle(Unit* u_); struct Unit { // a simple object with unique identifier Unit() { static int id_ = 0; id = id_++; } void act() { set_not_idle(this); } int id; }; // data std::vector<std::unique_ptr<Unit>> unit_storage; std::unordered_set<Unit*> unit_ptrs; std::unordered_map<int, Unit*> unit_from_id; std::unordered_set<Unit*> idle_units; Unit* get_unit_ptr(Unit u) { return unit_from_id[u.id]; } void set_idle(Unit* u_) { Unit* u = get_unit_ptr(*u_); // ensure we have pointer to unit in storage idle_units.insert(u); } void set_not_idle(Unit* u_) { Unit* u = get_unit_ptr(*u_); // ensure we have pointer to unit in storage idle_units.erase(u); } void add_unit(Unit u_) { unit_storage.push_back(std::make_unique<Unit>(u_)); Unit* u_ptr = unit_storage.back().get(); unit_ptrs.insert(u_ptr); unit_from_id[u_ptr->id] = u_ptr; // set map from id to pointer in storage set_idle(u_ptr); // units start as idle } void print() { std::cout << "Units in storage: "; for (auto a : unit_ptrs) { std::cout << a->id << " "; } std::cout << " Idle units: "; for (auto it = idle_units.begin(); it != idle_units.end(); ++it) { std::cout << (*it)->id << " "; } std::cout << std::endl; } int main() { srand(25); std::vector<Unit> units; // randomly populate our unit_storage with 8 units for (int i = 0; i < 50; i++) units.push_back(Unit()); for (int i = 0; i < 8; ) { int idx = rand() % units.size(); if (!get_unit_ptr(units[idx])) { add_unit(units[idx]); i++; } } print(); // get all idle units, and have them perform an action for (auto it = idle_units.begin(); it != idle_units.end(); ++it) { (*it)->act(); } print(); return 0; }
当前输出
Units in storage: 36 2 15 43 18 10 38 11 Idle units: 36 2 15 43 18 10 38 11 Units in storage: 36 2 15 43 18 10 38 11 Idle units: 2 15 43 18 10 38 11
预期结果是idle_units中无任何Unit剩余。尝试过不同迭代方式(如for (auto it : idle_units)循环、将迭代器移到循环体内等)均未解决问题,请问最优雅的解决方案是什么?
问题核心在于:当你通过erase(u)删除元素时,当前迭代器it会直接失效,后续的++it操作会触发未定义行为(示例中表现为循环提前终止,大型程序中则可能崩溃)。
最优解决方案:提前保存下一个迭代器
在调用act()(会删除当前元素)之前,先获取下一个元素的迭代器,确保即使当前迭代器失效,我们仍有有效的指针继续遍历:
修改主函数中的循环部分:
// 安全遍历并处理所有闲置Unit auto it = idle_units.begin(); while (it != idle_units.end()) { // 提前保存下一个迭代器,避免当前元素被删除后迭代器失效 auto next_it = std::next(it); (*it)->act(); it = next_it; }
为什么原循环会失效?
在原for循环中:
for (auto it = idle_units.begin(); it != idle_units.end(); ++it) { (*it)->act(); // 这里删除了it指向的元素,it直接失效 }
当act()调用erase删除当前元素后,it就变成了无效迭代器,后续执行++it时程序行为未定义——在你的示例中这导致循环直接终止,而在其他场景下可能触发崩溃。
可选方案:遍历集合副本
如果不想修改遍历逻辑,也可以先复制idle_units的副本,遍历副本并处理原集合:
std::unordered_set<Unit*> idle_copy = idle_units; for (auto unit : idle_copy) { unit->act(); }
这种方式逻辑简单,但缺点是当集合很大时会产生额外内存开销,适合元素数量较少的场景。
内容的提问来源于stack exchange,提问作者Apra24

