Pool¶
What is Pool?¶
Pool is a generic implementation of the object pool design pattern. It can store up to approximately 4 billion objects of the same type. All dynamic allocation is performed up front.
Performance Considerations¶
At present, Pool is actually a little slower than dynamic allocation on some modern systems, due to some overhead when deleting an object. While future versions may allevitate this, it is important to note that Pool’s main advantage is in providing safer dynamic memory access.
It is possible that Pool may offer better performance in environments where
dynamic allocation is extremely expensive. Running a comparative benchmark
between Goldilocks tests P-tB1601
and P-tB1601*
will indicate whether
there exist any mid-execution performance gains from using Pool in your
particular environment.
Technical Limitations¶
Pool can store a maximum of 4,294,967,294 objects. This is because it uses
32-bit unsigned integers for internal indexing, with the largest value
reserved as INVALID_INDEX
. The limit is calculated as follows.
2^{32} - 2 = 4,294,967,294
Including Pool¶
To include Pool, use the following:
#include "pawlib/pool.hpp"
Creating a Pool¶
A Pool
object can be created by whatever means is convenient. It handles
its own dynamic allocation internally when first created.
When the Pool is created, you must define its type and maximum size.
// Both of these methods are valid...
// Define a pool storing up to 500 Particle objects.
Pool<Particle> particles(500);
// Define a pool storing up to 100 Enemy objects.
Pool<Enemy>* baddies = new Pool<Enemy>(500);
Failsafe¶
By default, Pool will throw an e_pool_full
exception if you try to
add an object when the Pool is full. However, there are situations where
you may not want this behavior. You can initialize Pool in failsafe mode
to have Pool fail silently in this situation.
Note
Even in failsafe mode, Pool will still throw its other exceptions.
// Define a 500-object Particle pool in failsafe mode.
Pool<Particle> pool(500, true);
Failsafe mode does introduce another situation where an exception may be
thrown. If the Pool is full, adding an object will return an invalid reference.
One must either ensure that the reference is valid before using it, or catch
the e_invalid_ref
exception on Pool::access()
and Pool::destroy()
.
Using Pool¶
Pool References¶
One of the main features of Pool is its safety. When used properly, you don’t run the risk of memory errors, segmentation faults, or other undefined behaviors generally associated with dynamic allocation and pointers.
Instead of pointers, Pool offers pool references (pool_ref
). You can think
of these as “keys” - you cannot access anything in a Pool without the
appropriate pool_ref
.
Each pool reference is associated with a particular object in the Pool. When the object is destroyed, all the references to that object are invalidated to prevent undefinied behavior.
Important
The most important thing to remember is that you should never use pointers to access objects within the Pool.
Each reference is also directly associated with a single Pool. You cannot use a reference associated with one Pool to access an object in another Pool, even if both Pools are initialized identically.
Invalid References¶
A reference is invalid if:
It refers to a destroyed object.
It has only been created with its default constructor, with no assignment to it from
Pool::create()
. (It will also be considered Foreign to every Pool.)It was returned from
Pool::create
in failsafe mode, and the pool was full.
You can check if a reference is invalid by using the pool_ref::invalid()
function.
// This would be an invalid reference, since no object was created.
pool_ref<Foo> rf;
// This would return true.
rf.invalid();
Object Compatibility¶
To store an object in Pool, it must have a default constructor and a copy constructor. The copy constructor is used to provide indirect access to all the other constructors for the object.
Adding Objects¶
There are several ways to add a new object to the pool. In each one, the
important thing is that you wind up with a pool_ref
object. Watch this!
It is not possible to access or destroy an object within the Pool without its
reference.
All of the following methods are valid…
/* Foo contains a default constructor, a copy constructor, and a
* constructor that accepts an integer. */
class Foo;
Pool<Foo> pool(10);
try
{
pool_ref<Foo> rf1 = pool.create();
pool_ref<Foo> rf2 = pool.create(Foo(5));
pool_ref<Foo> rf3(pool);
pool_ref<Foo> rf4(pool, Foo(42));
}
catch(e_pool_full)
{
// Handle the exception.
}
Let’s break those down further.
The first method is to define a pool_ref
object, and assign the result
of Pool::create
function to it.
// Uses default constructor.
pool_ref<Foo> rf1 = pool.create();
// Uses copy constructor to indirectly access another constructor.
pool_ref<Foo> rf2 = pool.create(Foo(5));
You can also create the object by passing the Pool directly into the
pool_ref
’s constructor. This calls Pool.create() implicitly, so if
the Pool is not in failsafe mode, you still need to watch out for the
e_pool_full
exception.
// Uses default constructor.
pool_ref<Foo> rf3(pool);
// Uses copy constructor to indirectly access another constructor.
pool_ref<Foo> rf4(pool, Foo(42));
Accessing Objects¶
Objects are accessed within a Pool using the pool_ref
you got when
creating the object.
// The class Foo has a function "say()"
class Foo;
Pool<Foo> pool(10);
pool_ref<Foo> rf1 = pool.create();
/* We use the pool reference to access the object. Then we can
* interact with the object directly. */
pool.access(rf1).say();
The Pool.access()
function can throw two different exceptions.
The most common is e_pool_invalid_ref
. This is thrown when an invalid
pool reference is passed.
Pool<Foo> pool(10);
pool_ref<Foo> emptyRef;
pool.access(emptyRef); // throws e_pool_invalid_ref
The other is e_pool_foreign_ref
, which is thrown
if a pool reference that belongs to another pool is passed.
Pool<Foo> pool(10);
Pool<Foo> otherPool(10);
pool_ref<Foo> foreignRef = otherPool.create();
pool.access(foreignRef); // throws e_pool_foreign_ref
Destroying Objects¶
When you’re done with an object, you can remove it from the Pool. This frees
up space for another object to be created in its place later. To destroy
an object, simply pass a reference to it into Pool::destroy()
.
It’s important to note that if you have multiple references to the same object, they will all be invalidated when the object is destroyed.
Pool<Foo> pool(10);
pool_ref<Foo> thing(pool);
pool_ref<Foo> copyOfThing = thing;
// We destroy the object.
pool.destroy(thing);
pool.access(copyOfThing); // This will now throw e_pool_invalid_ref
Pool::destroy()
can throw e_pool_invalid_ref
or e_pool_foreign_ref
under the same circumstances as with Pool::access()
.
Exceptions¶
e_pool_full
¶
Cause: The Pool is full.
Thrown By: Pool::create()
(in non-failsafe mode)
e_pool_invalid_ref
¶
Cause: An invalid reference was used.
Thrown By: Pool::access()
, Pool::destroy()
e_pool_foreign_ref
¶
Cause: A reference from another pool was used, or a reference created
with its default constructor and not assigned to by Pool::create()
.
Thrown By: Pool::access()
, Pool::destroy()
e_pool_reinit
¶
Cause: Attempting to reinitialize an an object that already exists.
Thrown By: Internal - shouldn’t happen.
Examples¶
Enemy Pool¶
// Let's define an Enemy class for our example.
class Enemy
{
Enemy();
Enemy(const Enemy& cpy);
Enemy(std::string);
void attack();
void hurt(int);
void die();
int health;
~Enemy();
};
// Create our pool.
Pool<Enemy> baddies(500);
// This function would return the damage from the player's move.
int getPlayerMove();
void fightSkeleton()
{
/* Since our pool is not in failsafe mode, we must be on the lookout
* for the `e_pool_full` exception that create() can throw.*/
try
{
/* Create a new Enemy object in the pool. This uses Enemy's copy
* constructor to give access to the constructor accepting a string. */
pool_ref<Enemy> skeleton = pool.create(Enemy("Skeleton"))
}
catch(e_pool_full)
{
// We couldn't create the enemy, so just quit.
return;
}
while(baddies.access(skeleton).health > 0)
// We order our skeleton to attack the player.
baddies.access(skeleton).attack();
// The player hurts the skeleton.
baddies.access(skeleton).hurt(getPlayerMove());
}
// Make the skeleton character die.
baddies.access(skeleton).die();
// Destroy the skeleton object in the pool.
baddies.destroy(skeleton);
}
Particle System¶
class Particle
{
Particle();
Particle(const Particle& cpy);
Particle(int, int);
emit();
};
/* For this example, we'll define a failsafe pool, so we don't have to
* try/catch our creation of objects. */
Pool<Particle> particles(2000, true);
// Define a particle in the pool using its default constructor.
pool_ref<Particle> particle(particles);
void particleEffect(int type, int speed, int count)
{
/* One design pattern might be to generate a lot of particles in a loop.
* In this example, we'll store them in a FlexArray. */
FlexArray<pool_ref<Particle>> smoke_effect;
for(int i=0; i<count; ++i)
{
/* Define a particle in the pool using its copy constructor, which
* gives us access to the constructor that accepts an integer. */
smoke_effect.push(pool_ref<Particle>(particles, Particle(type, speed));
}
/* Let's emit our particles. */
for(int i=0; i<count; ++i)
{
// Ensure the particle exists before emitting it.
if(!smoke_effect[i].invalid())
{
particles.access(smoke_effect[i]).emit();
}
}
/* Destroy the particles when we're done. */
for(int i=0; i<count; ++i)
{
// Ensure the particle exists before destroying it.
if(!smoke_effect[i].invalid())
{
particles.destroy(smoke_effect[i]);
}
}
}