【问题标题】:vector of unique_ptr deleting?unique_ptr 的向量删除?
【发布时间】:2015-09-25 09:55:55
【问题描述】:

我有一个无法解决的段错误问题。它来自EntityManager,用于我正在开发的小型游戏引擎。 我可以添加Ship Entity,Ship 可以添加1 个Bullet Entity,但如果我尝试添加超过1 个Bullet,则会出现段错误。在过去的一天里,我一直在试图弄清楚这一点。下面是实际代码的一小段摘录。

#include <vector>
#include <memory>

struct EntityManager;
struct Entity {
    Entity(EntityManager* manager) : manager(manager) { }
    virtual ~Entity() { }
    virtual void update() = 0;

    EntityManager* manager;
};
struct EntityManager {
    void update() {
        for (auto& entity : entities) {
            entity->update();
        }
    }
    void add(Entity* e) {
        entities.emplace_back(e);
    }
    std::vector<std::unique_ptr<Entity>> entities;
};
struct Bullet : public Entity {
    Bullet(EntityManager* manager) : Entity(manager) { printf("Bullet ctor\n"); }

    virtual void update() override { }
};
struct Ship : public Entity {
    Ship(EntityManager* manager) : Entity(manager) { }

    virtual void update() override {
        printf("Adding Bullet\n");
        manager->add(new Bullet(manager));
    }
};
int main() {
    EntityManager manager;
    manager.add(new Ship(&manager));

    int loops{0};
    while (loops < 100) {
        manager.update();
        loops++;
        printf("Completed Loop #%d\n", loops);
    }
    return 0;
}

在实际代码中,一切都在它们自己的 .h/.cpp 文件中,并且是类而不是结构,但问题是一样的。 输出是`Adding Bullet // Bullet ctor // Completed Loop #1 // 添加 Bullet // Bullet ctor // Signal: SIGSEGV (Segmentation fault)

段错误发生在entity-&gt;update(); 行的EntityManager::update() 中。

【问题讨论】:

  • EntityManager 需要自定义移动操作来更新实体的 manager 指针。
  • 当您更新实体时,该循环会添加更多实体,从而使您的迭代器无效。当你在循环中时,你不能添加到你的向量中。
  • Write games not engines 是一本好书。也就是说,您可以通过事件或任何您认为合适的方式将这些操作延迟到更新循环之后,而不是立即添加或销毁,这样您就不会使迭代器无效。
  • 首选void add(std::unique_ptr&lt;Entity&gt; e) 而不是void add(Entity* e)
  • 用于提供最小工作示例的道具。

标签: c++ segmentation-fault game-engine stdvector unique-ptr


【解决方案1】:

问题是这个循环修改了向量:

    for (auto& entity : entities) {
        entity->update();
    }

当您修改向量以添加新元素时,您正忙于迭代它,这会使用于遍历容器的迭代器无效。

基于范围的for 循环被编译器扩展为:

auto begin = entities.begin(), end = entities.end();
for (; begin != end; ++begin)
  begin->update();

begin-&gt;update() 的调用会在向量中添加一个新元素,这会使容器中的所有迭代器无效,因此++begin 是未定义的行为。实际上,begin 不再指向向量(因为它已经重新分配并释放了begin 指向的旧内存)所以下一个begin-&gt;update() 调用取消引用无效的迭代器,访问释放的内存和段错误。

为了安全,您可能希望使用索引而不是迭代器:

for (size_t i = 0, size = entities.size(); i != size; ++i)
  entities[i].update();

这会捕获循环开始时的大小,因此只会迭代到循环开始时存在的最后一个元素,因此不会访问添加到末尾的新元素。

当向量被修改时,这仍然有效,因为您不存储迭代器或指向元素的指针,只存储索引。只要您不从向量中删除元素,即使在插入新元素后索引仍然有效。

【讨论】:

  • 我将循环更改为固定 for 循环,它通过迭代器访问 for(auto index{0u}; index
  • 不,这不安全。它不是“固定的 for 循环”,因为 vec.size() 发生了变化,并且它可以在添加新实体时永远循环访问它们。它恰好适用于您上面的示例,因为Bullet 不会修改向量,但您确实在第一个更新循环期间访问了这两个实体。我按照我的方式编写更正的循环是有充分理由的。正如我所说:“这会捕获循环开始时的大小,因此只会迭代到循环开始时存在的最后一个元素,因此不会访问添加到末尾的新元素。”
  • 你是对的,在运行了一些测试后,我发现它不像我上面提到的那样工作。正如您在回答中建议的那样,我将使用预先捕获的变量。老实说,我觉得我应该知道问题出在哪里。感谢您的帮助!
猜你喜欢
  • 2018-07-12
  • 2017-10-27
  • 2016-01-17
  • 2020-05-02
  • 1970-01-01
  • 1970-01-01
  • 2016-08-17
  • 2022-01-13
  • 2014-07-21
相关资源
最近更新 更多