5
\$\begingroup\$

I put together this little memory pool class to help avoid the costs associated with heap allocation and deallocation of objects that are frequently created & destroyed. It creates C++ standard shared_ptrs with custom deleters that return the pointed-to object to the memory pool where it can be recycled. Right now it uses the moodycamel queue to manage the pool, but it could be rewritten to use a simple vector or stack instead.

I'd appreciate any comments on design / implementation.

#ifndef SHAREDPTRMEMORYPOOL_H
#define SHAREDPTRMEMORYPOOL_H

#include <memory>
#include <3rdParty/spsc_queue/readerwriterqueue.h>

// Objects to be managed must have a default constructor 
// and a reconstruct(...) function which serves to initialize 
// their state (like a constructor) 
// 
// Use like:
// auto obj = make_shared_ptr_from_pool(my_memory_pool, my_reconstruct_args, ...);


template<class TPointedToClass>
class SharedPtrMemoryPool {
 public:
  explicit SharedPtrMemoryPool(unsigned int num_to_prealloc);
  ~SharedPtrMemoryPool();

  std::shared_ptr<TPointedToClass> get_shared_ptr();
  void release_shared_ptr(TPointedToClass* to_release);

 private:
  moodycamel::ReaderWriterQueue<TPointedToClass*> _pointer_queue;
};


template<class TPointedToClass>
SharedPtrMemoryPool<TPointedToClass>::SharedPtrMemoryPool(unsigned int num_to_prealloc) {
  for(unsigned int i = 0; i < num_to_prealloc; ++i) {
    auto prealloc_item = new TPointedToClass();
    _pointer_queue.enqueue(prealloc_item);
  }
}

template<class TPointedToClass>
SharedPtrMemoryPool<TPointedToClass>::~SharedPtrMemoryPool() {
  TPointedToClass* raw_ptr;
  while(_pointer_queue.try_dequeue(raw_ptr)) {
    delete raw_ptr;
  }
}

template<class TPointedToClass>
std::shared_ptr<TPointedToClass> SharedPtrMemoryPool<TPointedToClass>::get_shared_ptr() {
  TPointedToClass* raw_retval;
  bool recycle_success = _pointer_queue.try_dequeue(raw_retval);
  if(!recycle_success) {
    raw_retval = new TPointedToClass();
  }

  return std::shared_ptr<TPointedToClass>(raw_retval, [this](TPointedToClass* to_release) {
    this->release_shared_ptr(to_release);
  });
}

template<class TPointedToClass>
void SharedPtrMemoryPool<TPointedToClass>::release_shared_ptr(TPointedToClass* to_release) {
  _pointer_queue.enqueue(to_release);
}

template<class TPointedToClass, class... Args>
std::shared_ptr<TPointedToClass> make_shared_ptr_from_pool(SharedPtrMemoryPool<TPointedToClass>& pool, Args&&... args) {
  std::shared_ptr<TPointedToClass> retval = pool.get_shared_ptr();
  retval->reconstruct(std::forward<Args>(args)...);
  return retval;
}

#endif
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I think it would be more useful to give out unique pointers, which can be converted to shared pointers by the customer. The reverse isn't possible. Of course, the disadvantage to that is that the shared-pointer control block isn't pooled in the same way.

I think it would be better to hold uninitialised memory in our queue, so that we don't need the objects to have a default constructor or a reconstruct() function.

Keeping objects alive after the shared pointers' lifetime is surprising to users, who reasonably expect that after releasing all pointers, the object will relinquish all its resources (closing files, for example).

We seem to be performing much of the function of an allocator, so perhaps it's worth implementing the standard Allocator protocol?

The tests are not included in the review request, so it's hard to know what's verified correct and what might have been overlooked.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.