1

I've been trying to find a way to implement an object pool in my project. I've found a few examples on the internet and most of them use std::lists and an example of it in a book; Game development patterns and best practices that also use std::list.

I've been told, however, that it's kind of pointless to use lists for object pooling since returning an object back to the pool means that memory has to be allocated again.

I have also seen people utilize std::stack but I assume the same issue exists.

Here's the code I made for an object pool class following the book's example:

    /* This object has only an int data type (value).
       One method to se it and another one to print it. */
class Object
{
    public: 

        Object()
        {
            SetValue();
            std::cout << "Constructed/Reset." << std::endl;
        }

            // Copy constructor
        Object( Object& other )
        {
            this->mValue = other.mValue;
            std::cout << "Constructed." << std::endl;
        }

        ~Object()
        {
            std::cout << "Destroyed." << std::endl;
        }

        void SetValue( int val = -1 )
        {
            mValue = val;
        }

        void PrintValue()
        {
            std::cout << this->mValue << std::endl;
        }

    private:
        int mValue;

};

class Pool
{
    public:
        Pool()
        {
            std::cout << "Pool created!\n";
        }

        ~Pool()
        {
            std::cout << "Pool destroyed!\n";
        }

        void Fill( int size )
        {
            for( int i = 0; i < size; i++ )
            {
                Object* obj = new Object();
                pool.push_back( obj );
            }
            std::cout << "Objects created!" << "\nObjects in pool: "
                      << pool.size() << std::endl;
        }

        Object* AquireObject()
        {
            if( !pool.empty() )
            {
                Object* obj = pool.back();
                pool.pop_back();
                
                std::cout << "Object aquired!"
                          << "\nNumber of objects in pool: " << pool.size() << std::endl;

                return obj;
            }
            else
            {
                std::cout << "Object created and aquired!"
                          << "\nNumber of objects in pool: " << pool.size() << std::endl;
                return new Object();
            }
        }

        void ReleaseObject( Object* returningObject )
        {
            returningObject->SetValue();
            pool.push_back( returningObject );
            std::cout << "Object returned to the pool!"
                      << "\nNumber of objects in pool: " << pool.size() << std::endl;
        }

        void Clear()
        {
            while( !pool.empty() )
            {
                Object* obj = pool.back();
                pool.pop_back();
                delete obj;
            }
            std::cout << "Objects deleted!"
                      << "\nNumber of objects in pool: " << pool.size() << std::endl;
        }

    private:
        std::list<Object*> pool;
};

I also made a slightly different version that uses unique pointers instead:

    /* This object has only an int data type (value).
       One method to se it and another one to print it. */
class Object
{
    public: 

        Object()
        {
            SetValue();
            std::cout << "Constructed/Reset." << std::endl;
        }

            // Copy constructor
        Object( Object& other )
        {
            this->mValue = other.mValue;
            std::cout << "Constructed." << std::endl;
        }

        ~Object()
        {
            std::cout << "Destroyed." << std::endl;
        }

        void SetValue( int val = -1 )
        {
            mValue = val;
        }

        void PrintValue()
        {
            std::cout << this->mValue << std::endl;
        }

    private:
        int mValue;

};

    // Object pool using std::list
class Pool
{
    public:
        Pool() = default;
        ~Pool() = default;

            /* Fills the pool (list) with a given size using an Object class (previously defined)
               as a reference using its copy constructor.*/
        void Fill( int size, Object* obj )
        {
            for( int i = 0; i < size; i++ )
            {
                std::unique_ptr<Object> object = std::make_unique<Object>( *obj );
                pool.push_back( std::move( object ) );
            }
        }

            // Clears all items in the list.
        void PoolIsClosed()
        {
            pool.clear();
        }

            // Returns an instance of an object within the list.
        std::unique_ptr<Object> GetObject()
        {
            if( !pool.empty() )
            {
                std::unique_ptr<Object> object = std::move( pool.front() );
                pool.pop_front();
                
                std::cout << "Object retrieved." << "\nObjects in pool: "
                          << pool.size() << std::endl;
                
                return object;
            }
            else
            {
                /* "WARNING: NOT ALL CONTROL PATHS RETURN A VALUE." */
                std::cout << "Pool is empty." << std::endl;
            }
        }

            // Returns an instance back to the object list.
        void returnToPool( std::unique_ptr<Object> returningObject )
        {
            pool.push_back( std::move( returningObject ) );
            
            std::cout << "Object returned." << "\nObjects in pool: "
                      << pool.size() << std::endl;
        }

    private:
        std::list<std::unique_ptr<Object>> pool;
};

I want to know if an implementation like this is really pointless and if so, is there a simple-ish way to implement a basic object pool class?

I know that the boost library has its own implementation of an object pool... Has anyone of you used it before? I would love to know more about it, is it hard to use?..

I also found this object pool library: https://github.com/mrDIMAS/SmartPool which looks promising and kind of easy to use but I don't know how it works under the hood, it was a bit complicated for me to understand. Any opinions about this one?

AnotherBrian
  • 21
  • 1
  • 7
  • 1
    `std::stack` is usually implemented atop `std::vector`, so does sublinear allocations, rather than allocating every node like `std::list`. – Mooing Duck May 11 '21 at 22:43
  • 1
    `Fill` could be `std::generate_n(std::back_inserter(pool), size, [](){return std::make_unique(*obj);});` – Mooing Duck May 11 '21 at 22:46
  • 1
    A topical textbook is a best resources to learn, study, and understand these core algorithms, and computer science topics. You are definitely on the right track by using a textbook as your study guide. Don't distract yourself by a "few examples on the internet", or what self-appointed experts claim is "best practices"; or "being told", something or other, by "people [that] utilize" something else. Any clown can upload a video to Youtube spouting gibberish; or publish something dumb on a web site. You should ignore all that and just pay attention to your textbook. – Sam Varshavchik May 11 '21 at 23:01
  • Note that your `returnToPool` actually won't work. `returningObject` need to be passed as reference. – Ranoiaetep May 11 '21 at 23:43
  • @MooingDuck Oh ok I understand... I have seen less examples utilize std::stack but I it will be worth it looking deeper into it, thanks for the info!.. – AnotherBrian May 12 '21 at 00:59
  • @SamVarshavchik Hey thanks!.. You're absolutely right, after all I have learned most of what I know through books, thanks for the encouragement ;) – AnotherBrian May 12 '21 at 01:02
  • @Ranoiaetep Oh really?? I didn't know that, I'm just starting to learn about smart pointers and what not, I still need to look more into it. Thanks for the heads up! – AnotherBrian May 12 '21 at 01:03
  • Most use cases for a pool are to avoid lock contention of new/delete (malloc/free) as they access the global heap. If you truly don't have other threads at play then any "pool" will be slightly faster. While pools have their place, I've moved on to just using tcmalloc – Chad May 12 '21 at 02:15
  • @Ranoiaetep: `returnToPool` does not need a reference. It could use one, but that would make it "silently" steal ownership. Right now, it requires the caller to explicitly pass ownership, which is much safer. – Mooing Duck May 12 '21 at 15:57
  • @MooingDuck Then I would also explicitly use universal reference for the parameter. While they have the same effect, it's easier to tell you would need to move the object, and give clearer error message if you pass in a value type. – Ranoiaetep May 12 '21 at 17:11
  • @Ranoiaetep: You mean an rvalue reference for the parameter. Yes, that would be best. (Universal reference is a separate template concept that's bad for this case) – Mooing Duck May 12 '21 at 18:09

1 Answers1

0

If you don't mind rolling your own linked-list logic, you can make an ObjectPool class that doesn't use any data-structures, and therefore is guaranteed not to access the heap (except when its free-list is empty and it has to do so in order to return a new object, of course). Something like this (warning, barely tested code, may contain bugs):

#include <stdio.h>

template <typename Object> class ObjectPool
{
public:
   ObjectPool() : _head(NULL), _tail(NULL) {/* empty */}

   ~ObjectPool()
   {
      while(_head)
      {
         ObjectNode * next = _head->_next;
         delete _head;
         _head = next;
      }
   }

   Object * AcquireObject()
   {
      if (_head)
      {
         ObjectNode * oldNode = _head;  // what we're going to return to the user

         // Pop (oldNode) off the front of the free-list
         if (_head == _tail) _head = _tail = NULL;
         else
         {
            _head = _head->_next;
            if (_head) _head->_prev = NULL;
         }
         return &oldNode->_object;
      }
      else
      {
         // There's nothing in our free-list to re-use!  Hand the user a new object instead.
         ObjectNode * newNode = new ObjectNode;
         return &newNode->_object;
      }
   }

   // Note:  (obj) *must* be a pointer that we previously returned via AcquireObject()
   void ReleaseObject(Object * obj)
   {
      // reset (obj) back to its default-constructed state
      // (this might not be necessary, depending what your Objects represent)
      (*obj) = Object();

      ObjectNode * node = reinterpret_cast<ObjectNode *>(obj);   // this works only because _object is the first item in the ObjectNode struct
      if (_head)
      {
         // Prepend our node back to the start of our free-nodes-list
         node->_prev = NULL;
         node->_next = _head;
         _head->_prev = node;
         _head = node;
      }
      else
      {
         // We were empty; now will have just this one ObjectNode in the free-list
         _head = _tail = node;
         node->_prev = node->_next = NULL;
      }
   }

private:
   struct ObjectNode
   {
      Object _object;     // note:  this MUST be the first member-variable declared in the ObjectNode struct!
      ObjectNode * _prev;
      ObjectNode * _next;
   };

   ObjectNode * _head;  // first item in our free-nodes-list, or NULL if we have no available free nodes
   ObjectNode * _tail;  // last item in our free-nodes-list, or NULL if we have no available free nodes
};

// unit test
int main(int, char **)
{
   ObjectPool<int> test;
   int * i = test.AcquireObject();
   printf("i=%p\n", i);
   test.ReleaseObject(i);
   return 0;
}
Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234