Альтернативы объектному пулу?

Я не совсем уверен, что мне нужен объектный пул, но это кажется наиболее жизнеспособным решением, но с ним связаны некоторые нежелательные недостатки. Я делаю игру, в которой сущности хранятся в пуле объектов. Эти объекты не выделяются непосредственно с новым, а 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;
};

Если я уничтожу объект, его идентификатор будет добавлен в _destroyedEntitiesIndicies очередь, которая сделает так, что идентификатор будет использоваться повторно, и, наконец, его идентификатор будет установлен на 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-сущности, чтобы уничтожить их. Что если я смог получить сущность из пула или сохранить копию указателя на фактическую сущность? Как бы я 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

0

Решение

Если вы хотите 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;
}
}

Хотя это не полный пример (вам нужно позаботиться о правильных (копировать) конструкторах, операциях присваивания и т. Д. И т. Д.), Но он должен дать вам представление о том, о чем я говорю.

Однако я хочу подчеркнуть, что я все еще думаю, что простого пула достаточно для того, что вы пытаетесь сделать в этом контексте. Вы должны будете заставить остальную часть своего кода хорошо играть с объектами, чтобы они не использовали объекты, которые они не должны повторно использовать, но я думаю, что это легче сделать и поддерживать более четко, чем весь handle/weak pointer история выше.

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 ++ ’98, unique_ptr<> в C ++ — 11), которые гарантируют, что их внутренний указатель будет установлен в NULL при освобождении. Если вы объедините это с перегрузкой операторов new и delete в вашем классе Entity (см. Ниже), у вас может быть довольно мощный механизм. Есть некоторые варианты, такие как shared_ptr<>в новой версии стандарта C ++ — 11, которая также может быть вам полезна. Ваш конкретный пример:

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.com, википедия, и очень интересная статья из Травяной Затвор.

Альтернативы объектному пулу?

Пулы объектов создаются для того, чтобы избежать непрерывных манипуляций с памятью, что является дорогостоящим в тех ситуациях, когда известно максимальное количество объектов. Я не могу придумать альтернативы объектному пулу для вашего случая, я думаю, что вы пробуете правильный дизайн. Однако, если у вас много творений и разрушений, возможно, лучший подход — это не пул объектов. Невозможно сказать без экспериментов и измерения времени.

По поводу реализации существуют различные варианты.

Во-первых, неясно, испытываете ли вы преимущества в производительности, избегая выделения памяти, поскольку вы используете _destroyedEntitiesIndicies (вы в любом случае потенциально выделяете память каждый раз, когда уничтожаете объект). Вам придется поэкспериментировать с вашим кодом, если он дает вам прирост производительности в отличие от простого распределения. Вы можете попытаться удалить _destroyedEntitiesIndicies в целом и попытаться найти пустой слот только тогда, когда у вас кончается (_nextIndice> = 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

По вопросам рекламы [email protected]