【问题标题】:Avoiding iterator invalidation using indices, maintaining clean interface使用索引避免迭代器失效,维护干净的接口
【发布时间】:2013-12-08 11:16:04
【问题描述】:

我创建了一个MemoryManager<T> 类,它基本上是两个指针向量的包装器,用于管理堆分配对象的生命周期。

一个向量存储“活动”对象,另一个存储将在下一个MemoryManager<T>::refresh 添加的对象。

选择这种设计是为了避免在循环MemoryManager<T> 时使迭代器失效,因为直接向MemoryManager<T>::alive 向量添加新对象会使现有迭代器失效(如果它的大小增加)。

template<typename T> struct MemoryManager {
    std::vector<std::unique_ptr<T>> alive;
    std::vector<T*> toAdd;

    T& create() { 
        auto r(new T); 
        toAdd.push_back(r); 
        return *r; 
    }

    T& refresh() { 
         // Use erase-remove idiom on dead objects
         eraseRemoveIf(alive, [](const std::unique_ptr<T>& p){ return p->alive; });

         // Add all "toAdd" objects and clear the "toAdd" vector
         for(auto i : toAdd) alive.emplace_back(i); 
         toAdd.clear(); 
    }  

    void kill(T& mItem)  { mItem.alive = false; }

    IteratorType begin() { return alive.begin(); }
    IteratorType end()   { return alive.end(); }
}

我在我的游戏引擎中使用它来存储实体,并在每一帧更新每个“活着”的实体:

void game() {
    MemoryManager<Entity> mm;

    while(gameLoop) {
        mm.refresh();
        for(auto e : mm) processEntity(e);
        auto& newEntity = mm.create();
        // do something with newEntity
    }
}

这让我可以不断地创建/杀死实体,而不必过多担心它们的生命周期。


但是,我最近得出的结论是,没有必要使用两个 std::vector。我可以简单地使用单个向量并将迭代器存储到“最后一个存活对象”,在上述迭代器之后立即添加新创建的对象:

在我看来,这个想法很好用……但我实际上不能为 end 使用迭代器类型(如图所示),因为在向向量添加一些新元素后它可能会失效.我已经测试过了,这种情况经常发生,导致崩溃。

我能想到的另一个解决方案是使用索引而不是迭代器。这将解决崩溃问题,但我无法使用酷炫的 C++11 for(x : y) foreach 循环,因为 MemoryManager&lt;T&gt;::beginMemoryManager&lt;T&gt;::end 需要返回一个迭代器。

有没有办法用单个向量实现当前行为,并且仍然保持清晰的接口,可以与 C++11 for-each 循环一起使用?

【问题讨论】:

  • 无关:为什么toAdd不是智能指针向量?
  • @Casey:哎呀,你是对的。它应该是std::unique_ptr 的向量,因为销毁MemoryManager&lt;T&gt; 不会手动释放toAdd 的项目分配的内存。
  • 为什么不直接写IteratorType end() { return alive.begin() + aliveCount; }
  • @zch:因为在单次迭代期间,alive.begin() 迭代器可能会在更新期间添加新实体时失效。
  • @DyP:谢谢,修正了那个错字。

标签: c++ c++11 vector iterator invalidation


【解决方案1】:

获得稳定的迭代器(和引用)的最简单方法之一是使用std::list&lt;T&gt;。除非您需要 T 作为指向多态基类的指针,否则最好使用 std::list&lt;T&gt;,而不是 std::list&lt;std::unique_ptr&lt;T&gt;&gt;

另一方面,如果您的Entity 是多态基,则考虑使用std::vector&lt;std::unique_ptr&lt;T&gt;&gt;。尽管您不能依赖迭代器保持有效,但您可以依赖指针和对 Entity 的引用通过 std::vector&lt;std::unique_ptr&lt;T&gt;&gt; 保持有效。

在您的game() 示例中,您永远不会利用稳定的迭代器或指针。您也可以轻松(更简单)地这样做:

void game() {
    std::vector<Entity> mm;

    while(gameLoop) {
        mm.erase(std::remove_if(mm.begin(), mm.end(), [](const Entity& e)
                                                      { return e.alive; }),
                                                      mm.end());
        for(auto e : mm) processEntity(e);
        mm.push_back(create());
        auto& newEntity = mm.back();
        // do something with newEntity
    }
}

processEntity 循环期间,无法使迭代器无效。如果你这样做了,你最好不要使用基于范围的 for,因为结束迭代器只在循环开始时计算一次。

但是,如果您确实需要稳定的迭代器/引用,替换 std::list&lt;Entity&gt; 将非常容易。我会将erase/remove 改为使用list 的成员remove_if。效率会更高。

如果您这样做,性能测试(不是猜测)表明您的现有 MemoryManager 的性能受到影响,您可以使用“堆栈分配器”优化 list比如这里演示的:

http://howardhinnant.github.io/stack_alloc.html

这允许您预先分配空间(可以在堆栈上,也可以在堆上),并让您的容器从中分配。在预先分配的空间用尽之前,这将是高性能和缓存友好的。而且您仍然拥有迭代器/指针/引用的稳定性。

总结:

  1. 找出/告诉我们unique_ptr&lt;Entity&gt; 是否真的必要,因为Entity 是一个基类。首选container&lt;Entity&gt; 而不是container&lt;unique_ptr&lt;Entity&gt;&gt;

  2. 您真的需要迭代器/指针/引用稳定性吗?您的示例代码没有。如果您实际上不需要它,请不要为此付费。使用vector&lt;Entity&gt;(或vector&lt;unique_ptr&lt;Entity&gt;&gt;,如果必须)。

  3. 如果你真的需要container&lt;unique_ptr&lt;Entity&gt;&gt;,你能在牺牲迭代器稳定性的同时摆脱指针/引用的稳定性吗?如果是,vector&lt;unique_ptr&lt;Entity&gt;&gt; 是最好的选择。

  4. 如果您确实需要迭代器稳定性,强烈考虑使用std::list

  5. 如果您使用 std::list 并通过测试发现它存在性能问题,请使用适合您需求的分配器对其进行优化。

  6. 如果以上都失败了,那么开始设计你自己的数据结构。如果您走到这一步,请知道这是最困难的路线,并且一切都需要通过正确性和性能测试来支持。

【讨论】:

    【解决方案2】:

    您可以实现自己的迭代器类。

    以下内容可能会有所帮助。

    template <typename T, typename... Ts>
    class IndexIterator : public std::iterator<std::random_access_iterator_tag, T>
    {
    public:
        IndexIterator(std::vector<T, Ts...>& v, std::size_t index) : v(&v), index(index) {}
    
        // if needed.
        typename std::vector<T, Ts...>::iterator getRegularIterator() const { return v->begin() + index; }
    
        T& operator *() const { return v->at(index); }
        T* operator ->() const { return &v->at(index); }
    
        IndexIterator& operator ++() { ++index; return *this;}
        IndexIterator& operator ++(int) { IndexIterator old(*this); ++*this; return old;}
        IndexIterator& operator +=(std::ptrdiff_t offset) { index += offset; return *this;}
        IndexIterator operator +(std::ptrdiff_t offset) const { IndexIterator res (*this); res += offset; return res;}
    
        IndexIterator& operator --() { --index; return *this;}
        IndexIterator& operator --(int) { IndexIterator old(*this); --*this; return old;}
        IndexIterator& operator -=(std::ptrdiff_t offset) { index -= offset; return *this;}
        IndexIterator operator -(std::ptrdiff_t offset) const { IndexIterator res (*this); res -= offset; return res;}
    
        std::ptrdiff_t operator -(const IndexIterator& rhs) const { assert(v == rhs.v); return index - rhs.index; }
    
        bool operator == (const IndexIterator& rhs) const { assert(v == rhs.v); return index == rhs.index; }
        bool operator != (const IndexIterator& rhs) const { return !(*this == rhs); }
    
    private:
        std::vector<T, Ts...>* v;
        std::size_t index;
    };
    
    template <typename T, typename... Ts>
    IndexIterator<T, Ts...> IndexIteratorBegin(std::vector<T, Ts...>& v)
    {
        return IndexIterator<T, Ts...>(v, 0);
    }
    
    template <typename T, typename... Ts>
    IndexIterator<T, Ts...> IndexIteratorEnd(std::vector<T, Ts...>& v)
    {
        return IndexIterator<T, Ts...>(v, v.size());
    }
    

    【讨论】:

      【解决方案3】:

      您可以通过维护一个空闲列表来避免移动容器的元素(请参阅http://www.memorymanagement.org/glossary/f.html#free.list)。

      为避免对元素的引用无效,如果您不在中间插入或擦除,则可以使用 std::deque。 为避免迭代器失效,您可以使用 std::list。

      (感谢霍华德·欣南特)

      【讨论】:

      • 由于MemoryManager&lt;T&gt;被用于一些游戏的开发,我相信失去std::vector&lt;T&gt;的缓存友好性将是一个糟糕的性能损失
      • @VittorioRomeo:你应该衡量一下性能。 vector&lt;T&gt; 缓存友好。但你没有那个。你有vector&lt;unique_ptr&lt;T&gt;&gt;,它对缓存不太友好。你试过list&lt;T&gt;forward_list&lt;T&gt;吗?这些容器擅长保持迭代器有效。 deque&lt;T&gt; 擅长维护稳定的引用,但前提是你不插入到中间。但是deque&lt;T&gt;vector&lt;T&gt; 更频繁地使迭代器无效。
      • @VittorioRomeo Good deque(如boost::container::deque)是缓存友好的——它将元素连续存储在大小是缓存行大小的倍数的块中。 deque 的顺序遍历是缓存友好的,它只比vector 慢一点(因为需要检查块边界)。主要的性能差异在于随机访问成本 - deque 每次索引访问都需要额外的内存负载。另一个性能差异是分配总数(通常deque 需要 O(N) 次分配,而vector 需要 O(log N))。
      【解决方案4】:

      您可以实现自己的迭代器类,以您喜欢的方式处理事情。然后您的 begin() 和 end() 可以返回该类的实例。例如,您的自定义迭代器可以存储整数索引和指向向量本身的指针,从而使迭代器即使在重新分配时也保持有效。

      【讨论】:

      • 是否有使用 C++11 的自定义迭代器的完整示例?我在使用 eraseremove_if 时遇到了很多麻烦,而且模板错误变得非常混乱
      猜你喜欢
      • 2017-06-21
      • 2015-12-23
      • 1970-01-01
      • 1970-01-01
      • 2017-07-15
      • 1970-01-01
      • 1970-01-01
      • 2013-10-24
      • 1970-01-01
      相关资源
      最近更新 更多