General Template Usage with Pointers
Monday, May 14th, 2012This evening I finished up adding pools to DKit, and there are a few things that make it so bloody cool I had t write about it. In short, the problem is one that I've had to deal with before: How to make a simple pool of things such that I only create what I need, but don't over create, and can use a manageable set of things from a pool.
Like say I wanted to have a messaging system. I might want to have a bunch of std::string values that are created with a minimum size to make it easy to move things around. Then, all I would need to do is to get one from the pool, clear it out, add in the data, and then when I'm done, return it to the pool. The location in the code that gets from the pool is at a totally different place than the place that recycles these instances, and it's possible that due to threading or queueing, we may need to have several in play all at once. But eventually they will all come back to be recycled.
In the past, I had one class for a std::string pool, and another for datagrams, and so on. Each of these was almost identical to the other, just in how the instance was created. Typically, I'd have some sense of the 'default' size of the container I was creating. I based each off a single type of FIFO queue, so that it would always be a SP/SC pool, etc. This was necessary because I hadn't super classed the FIFO queues as I have in DKit.
Still… this was a lot of copy-n-paste reuse, and it was clear that it wasn't anywhere near as flexible as it could be. So today I decided to try and see what I could get away with if I tried to make a complete template pool class. The challenges were pretty clear:
- Include the Type of the Queue in the Template - I knew that now that I had the FIFO abstract template class in DKit, it was going to be possible to have the constructor make a queue of the right type, and then just "use it" via the FIFO abstract template class and be able to allow the user to define what access type they wanted for the pool.
- Include the Max Size of the Pool as a Power of Two - this was in keeping with the queues I'd be creating, and so shouldn't prove to be too hard.
- Allow Pointers and Non-Pointers to be Pooled - this was the biggest challenge I faced, to be sure. In the pool, I wanted to have two basic methods: next() to get a new one from the pool, and recycle(T) to return it to the pool. The problem is what if the type 'T' is a pointer versus a non-pointer? How do we make sure we can delete a pointer, but simply let a uint64_t fall on the floor and be cleaned up?
Thankfully, I was able to look on Stack Overflow and see this interesting question which lead me to the answer I needed. What it really boiled down to was that I could use the boost::is_pointer(), but that only really lets me know if I need to clean up the contents of the queue in the clear() method. The trick to constructing and destructing was to realize that the templates worked outside the scope of the class definition, and so after the class definition, I added in these functions:
namespace dkit { namespace pool_util { /** * In order to handle both pointers and non-pointers as data * types 'T' in the pool, we need to take advantage of the * template methods and make create() and delete() methods * for pointers and non-pointers. * * For create(), it's pretty easy - we allow for nothing to be * done for the non-pointer, and a standard 'new' for the pointer. * For delete(), it's the same - we delete it and then NULL it * out if it's a pointer, if it's not, we do nothing. */ template <typename T> void create( T t ) { } template <typename T> void create( T * & t ) { t = new T(); } template <typename T> void destroy( T t ) { } template <typename T> void destroy( T * & t ) { if (t != NULL) { delete t; t = NULL; } } } // end of namespace pool_util } // end of namespace dkit
Then, in the critical next() and recycle() methods, I simply used these functions:
/** * This method is called to pull another item from the pool, or * create a new one if nothing is in the pool. This is the classic * way of getting the "next" item to work with. */ T next() { T n; // see if we can pop one off the queue. If not, make one if ((mQueue == NULL) || !mQueue->pop(n)) { pool_util::create(n); } // return what we have - new or used return n; } /** * This method is called when the user wants to recycle one of * the items to the pool. If the pool is full, then we'll simply * delete it. Otherwise, we'll put it back in the pool for use * the next time. */ void recycle( T anItem ) { if ((mQueue == NULL) || !mQueue->push(anItem)) { pool_util::destroy(anItem); } }
By using the create(n) and destroy(n) functions, I allow the compiler to see the template functions and pick which one to use. In the case of a pointer for 'T', it chooses the pointer-reference argument, in the case of a non-pointer, it's the pass-by-value argument. This selectivity allows me to partially implement these any time I want in order to make the actual construction and destruction as complicated as I need without requiring it for the simple default constructor and destructor.
Once I had this, the remaining problems weren't too bad at all.
I created an enum for the type of access to use:
/** * We need to have a simple enum for the different "types" of queues that * we can use for the pool - all based on the complexity of the access. This * is meant to allow the user to have complete flexibility in how to ask for, * and recycle items from the pool. */ namespace dkit { enum queue_type { sp_sc = 0, mp_sc, sp_mc, }; } // end of namespace dkit
and then it was pretty simple to make the template and the constructor:
template <class T, uint8_t N, queue_type Q> class pool { public: pool() : mQueue(NULL) { switch (Q) { case sp_sc: mQueue = new spsc::CircularFIFO<T, N>(); break; case mp_sc: mQueue = new mpsc::CircularFIFO<T, N>(); break; case sp_mc: mQueue = new spmc::CircularFIFO<T, N>(); break; } } … };
In the end, the code worked wonderfully. I built a test app that made sure the destructor was properly being called when recycle() was being called and the queue was full - check. I also made sure that when the pool was destructed, any remaining elements were properly destructed - if they were pointers.
It was really an amazing little bit of code. This is far more flexible and better than the previous single-purpose pools I've written. Less code is always better.