To see the full documentation of the interface please follow the links:
Below is some discussion about their implementations and requirements.
object_pool
There is only one class that you should use frequently, and another that you might use from time to time. Namely these are object_pool
and acquired_object
. The former represents the pool of shared instances and the latter the acquired versions of these instances.
The actual declaration of object_pool
is this:
template <
class T,
class Allocator = std::allocator<T>,
class Mutex = std::mutex
>
class object_pool;
which means that you can select which type of allocator you want to use. As long as it satisfies the requirements of Allocator. In particular, we need these methods in order to do the allocations:
T* allocate(size_type n)
deallocate(T* p, size_type n)
For the requirements of the parameter Mutex
, we ask that it satisfies the requirements of Lockable. We give the option to choose these types because more often than not, development teams will want to run their own custom allocators and/or mutexes since they may be optimized to their needs. Unfortunately, we need to specify the interface they follow and so, we follow each concept’s specification. If you’re not worried about which implementation of Allocator
or Mutex
is used, then object_pool
will use the STL implementations.
Internally, the implementation uses contiguos allocated memory to store the managed objects. This allows the pushing of r-value
and l-value
references into the pool and the in-place construction, i.e.
std::string my_string = "Hello World!";
object_pool<string> pool;
pool.push(my_string); // pushes l-value reference, will copy the string
pool.push(std::string("Hello World")); // pushes r-value reference, will move the string
pool.emplace("Hello World") // constructs a string, will forward the arguments for construction
This then leaves the complexity up to the constructors of T
.
For the acquisition of resources, the pool keeps a stack of pointers std::stack<T*>
, which point to the individual blocks of memory where all the objects are stored. This means that all we need to do, is to return the top()
of the stack and pop()
the pointer. Effectively, we get constant complexity for the acquisition of objects. Hurray! However, there is but one problem. We need to keep track of the pointers after they’ve left the pool. Now, there may be a lot of different methods to achieve this, but by far what I consider the best is RAII. By wrapping around a helper class that takes care of the pointer we can achieve memory safety. In this case, our helper class is acquired_object
. This class takes care of seeing that the pointer returns safely to the instance of object_pool
. When one makes a call to acquire
, it will construct an acquired_object
. When the acquired_object
is destroyed, the pointer will return to the pool where it can be safely managed and deleted.
Now I know what you’re thinking after you’ve read the last part. When does the memory get freed? What if I’ve got a main thread and a child thread that both use the pool? What if I’ve the pool goes out of scope in the declaring thread and I’ve got acquired objects in other threads? Will all acquired objects get invalidated? The answer for this last question is a big NO. The memory of all the stored objects will only be freed until the last remaining acquired_object
goes out of scoped or gets destroyed. The pool achieves this by storing a shared_ptr
to itself. When each acquired_object
gets constructed, it gets passed two parameters:
shared_ptr
to the poolThis means that the shared_ptr
that the acquired_object
has, owns the object_pool
and so it is obvious that only the last shared_ptr
will be the one to delete the pool.
acquired_object
The class acquired_object
The class acquired_object
is actually a member of object_pool
. This means that you need to prepend object_pool<T, Allocator, Mutex>::
if you wanted to declare an object. For example, if you wanted to declare an empty acquired_object
and then access it, you would need to do this:
using namespace std;
using namespace carlosb;
...
object_pool<string>::acquired_object empty_obj; // no object acquired
object_pool<string> pool(10, "Not so empty anymore!"); // pool of 10 default-constructed objects
empty_obj = pool.acquire();
cout << *empty_obj << "\n";
Output:
Not so empty anymore!
This is all good, until you try to access an empty object. Say you had the following program, where you declare an empty acquisition:
using namespace std;
using namespace carlosb;
...
object_pool<string>::acquired_object empty_obj; // no object acquired
cout << *empty_obj << "\n"; // std::logic_error, you can't access an empty uninitialized object
However, this example shows that it is pretty clear there will be an error. If you’ve read the documentation, you’ll know that acquire()
can’t always acquire an object but refunding that it will not throw an exception. Instead, it relies on the fact that you check whether the object is valid through the following dialect:
if (obj)
doSomething(*obj);
else
doSomethingElse();