如何从使用向量大小的for循环中删除向量元素

3

我有一个游戏,在这个游戏中,我可以向物体发射子弹,当子弹击中物体或离开屏幕时,这个物体和这颗子弹都将被删除。

例如:

std::vector<object> object_list;
for(size_t i = 0; i < object_list.size(); i++)
{

    if(object_list[i].hit())
    {
        object_list.erase(object_list.begin() + i);
    }
    else
        object_list[i].draw();
}

这样做的问题在于,当我移除一个对象时,向量的大小会减小,因此当检查条件时,它会失败并出现错误,如“vector subscript out of range”。我可以选择不渲染被击中的陨石,而是渲染那些未被击中的,但这样做的问题在于被击中的物体数量会增加(分裂),所以最终程序会变慢。我已经在屏幕外的子弹使用了类似的概念,但我找不到解决方法或更好的删除元素的方法。
对象和子弹都是类。

1
我怀疑错误是在其他地方产生的 - 那个循环应该只是在向量中留下太多元素。(假设你有 v = {1,2,3,4, ...} 并想要删除所有内容。你删除 v[0] 并得到 v = {2,3,4, ...}。然后你删除 v[1] 并得到 {2,4, ...}。以此类推。) - molbdnilo
1
@furrylion 反向遍历向量。然后删除元素不会造成任何问题。 - Sai Sreenivas
2
避免循环问题的一种可靠方法是完全消除循环并使用标准算法。在这种情况下,std::remove_if(object_list.begin(), object_list.end(), [](const object &o) {return o.hit();}) 就能解决问题(假设使用 C++11 或更高版本)。它还可以避免您的循环存在的问题,包括不检查/删除紧随已删除元素之后的元素。 - Peter
7个回答

5

你应该将for循环分为两部分:

  1. 移除所有“hit”元素:
object_list.erase(std::remove_if(object_list.begin(), 
object_list.end(), [](auto&& item) { return item.hit(); }),
object_list.end());
  1. 绘制剩余部分:
std::for_each(object_list.begin(), object_list.end(), [](auto&& item) { item.draw(); });

更加安全和易读。


2
你可以这样做:

for (size_t i = 0; i < object_list.size(); )
{
    if (object_list[i].hit())
        object_list.erase(object_list.begin() + i)
    else
    {
        object_list[i].draw()
        ++i;
    }
}

2
与其他答案相同的想法,但是这个代码使用迭代器更容易一些。
for (auto i = object_list.begin(); i != object_list.end(); )
{
    if (i->hit())
    {
        i = object_list.erase(i);
    }
    else
    {
        i->draw();
        ++i;
    }
}
< p > vector::erase 返回一个迭代器,指向下一个元素,您可以使用它继续循环。


2

使用range-v3库(C++20)的函数式方法

[...] 我正在寻找一个解决方案,以更好的方式删除元素。

使用ranges::actions::remove_if操作来自于range-v3库,你可以使用函数式编程风格的方法在原地改变object_list容器:

object_list |= ranges::actions::remove_if(
    [](const auto& obj) { return obj.hit(); });

紧接着后面的ranges:for_each调用,用于绘制该对象:
ranges::for_each(object_list, [](const auto& obj){ obj.draw(); });

演示


这解决了为什么原始代码不能按预期工作的问题,只是一个副作用,但是很好的解决方案 ;) - codeling
@codeling 这确实回答了问题,但问题的作者在一定程度上隐藏了我回答的部分。我添加了一个引用来强调这个答案所涉及的部分。 - dfrib
你说得对!仍然有一个问题,那就是OP的编译器是否已经支持C++20,以及如何启用它:D - codeling

0
假设您在i=5处,该对象已被击中,在删除该元素后,i=6处的对象将被移动到i=5处,而您尚未对其进行检查,因此请在erase语句后添加i--;
另一种方法是-
for(size_t i = 0; i < object_list.size();)
{
 if(object_list[i].hit())
 {
     object_list.erase(object_list.begin() + i);
 }
 else
 {
     object_list[i].draw();
     i++;
 }
}

此外,从向对象标记为命中的代码执行的向量中删除对象可能会更快,这样您只需要绘制列表中剩下的所有对象。了解您如何完成所有这些工作的更多背景信息将有助于决定更好的特定事项 :)

0

所示代码并不会失败或给出向量下标超出范围的错误 - 它只是没有考虑每个对象,因为它跳过了删除后的元素。

对于使用C++11及更高版本概念的非常简短和简洁的解决方案,请参见Equod的答案dfri的答案

为了更好地理解问题,和/或如果您必须坚持使用带有索引的for循环,您基本上有两个选择:

  1. 以相反的方向迭代向量(即从最后一个元素开始),然后移动当前元素之后的项目不是问题;
for (int i=object_list.size()-1; i>=0; --i)
{
    if (object_list[i].hit())
    {
        object_list.erase(object_list.begin() + i)
    }
    else
    {
        object_list[i].draw()
    }
}

或者,如果顺序很重要(例如绘制项目),并且您必须从前到后迭代,则仅在您未擦除当前元素时增加计数器i
for (int i=0; i<object_list.size(); /* No increase here... */ )
{
    if (object_list[i].hit())
    {
        object_list.erase(object_list.begin() + i);
    }
    else
    {
        object_list[i].draw();
        ++i;   // ...just here if we didn't remove the element
    }
}

0
我怀疑std::vector不是你想要的容器(当然,我不知道整个代码)。每次调用erase都意味着向量右侧的重新分配(以及对象的复制),这可能非常昂贵。而你实际的问题是设计问题的症状。
从我看到的,std::list可能更好:
std::list<object> objects;
// ...
for(std::list<object>::iterator it = objects.begin(); it != objects.end();)
{
    if(it->hit())
      objects.erase(it++); // No object copied
    else
    {
      it->draw();
      ++it;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接