2
\$\begingroup\$
template<typename T>
class object_pool {

public:
  explicit object_pool(std::size_t initial_size = 1048u);

  object_pool(const object_pool&) = delete;

  object_pool(object_pool&&) = delete;

  ~object_pool();

  object_pool& operator=(const object_pool&) = delete;
  object_pool& operator=(object_pool&&) = delete;

  template<typename... Args>
  std::unique_ptr<T, std::function<void(T*)>> request(Args&&... args);

private:
  std::deque<T*> _object_pool; // is deque the best container?

};

template<typename T>
object_pool<T>::object_pool(std::size_t initial_size) : _object_pool() {
  for (std::size_t i = 0; i < initial_size; ++i) {
    T* object = reinterpret_cast<T*>(::operator new(sizeof(T)));
    _object_pool.push_back(object);
  }
}

template<typename T>
object_pool<T>::~object_pool() {
  while (!_object_pool.empty()) {
    T* object = _object_pool.front();
    _object_pool.pop_front();
    ::operator delete(object);
  }
}

template<typename T>
template<typename... Args>
std::unique_ptr<T, std::function<void(T*)>> object_pool<T>::request(Args&&... args) {
  T* object = nullptr;

  if (!_object_pool.empty()) {
    object = new(_object_pool.front()) T(std::forward<Args>(args)...);
    _object_pool.pop_front();
  } else {
    object = new T(std::forward<Args>(args)...);
  }

  auto object_pointer = std::unique_ptr<T, std::function<void(T*)>>(object, [this](T* object){ 
    object->~T();
    _object_pool.push_back(object);
  });

  return object_pointer;
}

My goal is to write a very lightweight object pool for another project. The _object_pool is supposed to just hold raw uninitialized pointers to T, which I then initialize when an object is requested. Likewise, i call the destructor on the returned instances before putting them back into the _object_pool so that resources used by them can be freed. I am also allocating space for a set ammount of instances when I create the pool and only request new memory when all object from the _object_pool are in use.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ If you have unit tests for this code it would help us review the code if they were posted as well. \$\endgroup\$
    – pacmaninbw
    Commented Apr 12, 2021 at 16:27

1 Answer 1

1
\$\begingroup\$

unique_ptr ownership / lifetime issues:

Normally, std::unique_ptr indicates single ownership of an object and thus gives the user certain expectations of how they can safely use it. But here the object_pool "owns" the object (or at least the underlying memory), so the use of unique_ptr is rather misleading. Consider the following code:

{
    auto ptr = std::unique_ptr<int, std::function<void(int*)>>();

    {
        object_pool<int> pool;
        ptr = pool.request(5);
    }

    // uh oh...
}

We would normally expect the above code to work without problems. However, the custom unique_ptr deleter will attempt to call _object_pool.push_back(object) on a pool that doesn't exist any more.

At the very least, I'd suggest "hiding" the use of unique_ptr behind a typedef, and documenting thoroughly that it must go out of scope before the object pool:

template<class T>
using object_pool_ptr = std::unique_ptr<T, std::function<void(T*)>>;

This also means the user doesn't have to worry about typing out the custom deleter signature.

It would be even better to make object_pool_ptr a wrapper class around the unique_ptr to hide it completely.


object_pool destructor:

As a debugging feature, the object_pool could keep track of the total number of objects allocated. We could then check that our "free list" (the _object_pool deque) is the correct size in the object_pool destructor, rather than waiting until a pointer goes out of scope.

I don't think it's necessary to call pop_front() on individual elements in the destructor. We could call clear() after the loop, or allow the _object_pool to clean up naturally in its own destructor.


[Note: below this point are just possible things that might be useful, depending on what other features your object pool needs, and what you're actually using it for].


tracking allocated memory inside the pool:

This "object pool is a free-list" strategy is really clever... but maybe there are advantages to keeping track of all the allocated memory inside the object pool too?

We could use a std::deque<std::aligned_storage_t<sizeof(T)>> member variable allowing us to call push_back to add more elements without invalidating references (and hence pointers) to the original elements.

This also means we avoid the need to call global operators new and delete for every element.


manual lifetime (but not memory) management:

If the object pool keeps track of all allocated memory as above, perhaps it's ok to give the user a simple raw pointer to an allocated object and expect them to call an object_pool.destroy(ptr); function when they're done with it?

This isn't dangerous in terms of object lifetimes, since the object pool can still call the object destructors when necessary (and maybe issue a warning or assert() if desired). Although it would open up the possibility of pointers to destroyed objects being accidentally reused.

It also allows us to make the object_pool moveable, and add a clear() function.


\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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