对象池的替代品?

时间:2013-06-30 10:03:53

标签: c++ oop design-patterns pool

我不太确定我需要一个对象池,但它似乎是最可行的解决方案,但有一些与之相关的不想要的缺点。我正在制作游戏,其中实体存储在对象池中。这些实体不是直接用new分配的,而是std::deque为它们处理内存。

这就是我的对象池或多或少的样子:

struct Pool
{  
    Pool()
        : _pool(DEFAULT_SIZE)
    {}

    Entity* create()
    {
        if(!_destroyedEntitiesIndicies.empty())
        {
            _nextIndex = _destroyedEntitiesIndicies.front();
            _destroyedEntitiesIndicies.pop();
        }

        Entity* entity = &_pool[_nextIndex];
        entity->id = _nextIndex;
        return entity;
    }

    void destroy(Entity* x)
    {
        _destroyedEntitiesIndicies.emplace(x->id);
        x->id = 0;
    }

private:

    std::deque<Entity> _pool;
    std::queue<int> _destroyedEntitiesIndicies;
    int _nextIndex = 0;
};

如果我销毁一个实体,它的ID将被添加到_destroyedEntitiesIndicies队列中,这将使得ID将被重新使用,最后它的ID将被设置为0.现在唯一这样做的陷阱是,如果我销毁一个实体然后立即创建一个新实体,之前销毁的实体将被更新为刚刚创建的实体。

Entity* object1 = pool.create(); // create an object
pool.destroy(object1); // destroy it

Entity* object2 = pool.create(); // create another object
// now object1 will be the same as object2

std::cout << (object1 == object2) << '\n'; // this will print out 1

这对我来说似乎不对。我该如何避免这种情况?显然上面的内容可能不会发生(因为我会将对象破坏延迟到下一帧)。但这可能会导致一些干扰,同时将实体状态保存到文件中,或者沿着这些行保存。

修改

假设我做了NULL实体来销毁它们。如果我能从池中获取实体,或者将指针的副本存储到实际实体,该怎么办?如何在销毁时将所有其他重复实体清空?

Pool pool;
Entity* entity = pool.create();

Entity* theSameEntity = pool.get(entity->getId());

pool.destroy(entity);

// now entity == nullptr, but theSameEntity still points to the original entity

2 个答案:

答案 0 :(得分:1)

这个问题似乎有各种各样的部分。我们来看看:

  

(...)如果我销毁一个实体然后立即创建一个新实体,   先前销毁的实体将更新为   刚刚创建的同一个实体。这对我来说似乎不对。怎么样   我能避免这种情况吗?

您可以修改此方法:

void destroy(Entity* x)
    {
        _destroyedEntitiesIndicies.emplace(x->id);
        x->id = 0;
    }

成为:

void destroy(Entity *&x)
    {
        _destroyedEntitiesIndicies.emplace(x->id);
        x->id = 0;
        x = NULL;
    }

这样,您就可以避免遇到的具体问题。但是,它无法解决整个问题,您可以随时拥有不会更新为NULL的副本。

另一种方法是使用auto_ptr<>(在C ++中的C ++&#39; 98,unique_ptr<>),这保证了它们的内部指针在释放时将被设置为NULL。如果将它与您的Entity类中的运算符new和delete的重载相结合(见下文),您可以拥有一个非常强大的机制。在新版本的标准C ++ - 11中有一些变体,例如shared_ptr<>,它们对您也很有用。您的具体示例:

auto_ptr<Entity> object1( new Entity ); // calls pool.create()
object1.release();                      // calls pool.destroy, if needed

auto_ptr<Entity> object2( new Entity ); // create another object

// now object1 will NOT be the same as object2
std::cout << (object1.get() == object2.get()) << '\n'; // this will print out 0

您有各种可能的信息来源,例如cplusplus.comwikipedia以及来自Herb Shutter的非常有趣的文章。

  

对象池的替代方案?

创建对象池是为了避免在已知最大对象数的情况下进行连续的内存操作,这种操作很昂贵。对于你的情况我没有其他对象池的替代品,我认为你正在尝试正确的设计。但是,如果你有很多创作和破坏,也许最好的方法不是对象池。没有实验和测量时间就不可能说。

关于实施,有各种选择。

首先,目前尚不清楚您是否通过避免内存分配来体验性能优势,因为您正在使用_destroyedEntitiesIndicies(无论如何,每次销毁对象时都可能会分配内存)。如果与普通分配相比,这会为您提供足够的性能提升,那么您必须尝试使用​​代码。您可以尝试完全删除_destroyedEntitiesIndicies,并尝试仅在用完时找到空插槽(_nextIndice&gt; = DEFAULT_SIZE)。要尝试的另一件事是丢弃在这些空闲插槽中浪费的内存并改为分配另一个块(DEFAULT_SIZE)。

同样,这完全取决于您正在经历的实际用途。找出答案的唯一方法是试验和测量。

最后,请记住,您可以修改类Entity,以便透明地支持对象池。这样做的一个好处是你可以试验它是否是一种更好的方法。

class Entity {
public:
    // more things...
    void * operator new(size_t size)
    {
        return pool.create();
    }

    void operator delete(void * entity)
    {
    }

private:
    Pool pool;
};

希望这有帮助。

答案 1 :(得分:1)

如果您希望Entity实例只能通过create访问,则必须隐藏get函数(原始代码中不存在:) :))。

我认为在你的游戏中添加这种安全性有点过分,但如果你真的需要一种机制来控制对内存中某些部分的访问,我会考虑返回类似handle的内容。 weak pointer而不是原始指针。这个weak pointer将包含一个向量/映射的索引(存储在除weak pointer之外的任何地方都无法访问的地方),而索引又包含实际的Entity指针和一个小的哈希值指示弱指针是否仍然有效。

这是一些代码,所以你明白我的意思:

struct WeakEntityPtr; // Forward declaration.
struct WeakRefIndex { unsigned int m_index; unsigned int m_hash; }; // Small helper struct.
class Entity {
    friend struct WeakEntityPtr;
private:
    static std::vector< Entity* > s_weakTable( 100 );
    static std::vector< char > s_hashTable( 100 );
    static WeakRefIndex findFreeWeakRefIndex(); // find next free index and change the hash value in the hashTable at that index
struct WeakEntityPtr {
private:
    WeakRefIndex m_refIndex;
public:
    inline Entity* get() {
        Entity* result = nullptr;

        // Check if the weak pointer is still valid by comparing the hash values.
        if ( m_refIndex.m_hash == Entity::s_hashTable[ m_refIndex.m_index ] )
        {
            result = WeakReferenced< T >::s_weakTable[ m_refIndex.m_index ];
        }

        return result;
    }
}

这不是一个完整的例子(你必须照顾正确的(复制)构造函数,赋值操作等等)但它应该让你知道我在说什么。

但是,我想强调的是,我仍然认为一个简单的池就足以满足您在该环境中要做的事情。您将不得不使其余的代码与实体很好地协作,这样它们就不会重用它们不应该重用的对象,但我认为这样做更容易,并且可以比整个{{{ 1}} / handle上面的故事。