【问题标题】:Correct object pool implementation in C++?C++ 中正确的对象池实现?
【发布时间】:2021-05-11 22:39:52
【问题描述】:

我一直在尝试找到一种在我的项目中实现对象池的方法。我在互联网上找到了一些示例,其中大多数使用 std::lists 以及书中的示例;也使用 std::list 的游戏开发模式和最佳实践。

然而,有人告诉我,将列表用于对象池是毫无意义的,因为将对象返回池意味着必须再次分配内存。

我也看到人们使用 std::stack 但我认为存在同样的问题。

这是我按照本书的示例为对象池类编写的代码:

    /* 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;
};

我还制作了一个稍微不同的版本,它使用了唯一指针:

    /* 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;
};

我想知道这样的实现是否真的毫无意义,如果是,是否有一种简单的方法来实现基本的对象池类?

我知道 boost 库有自己的对象池实现……你们中有人用过吗?我很想知道更多关于它的信息,它很难使用吗?..

我还发现了这个对象池库:https://github.com/mrDIMAS/SmartPool,它看起来很有前途,而且很容易使用,但我不知道它是如何工作的,我理解起来有点复杂。对此有何看法?

【问题讨论】:

  • std::stack 通常在std::vector 之上实现,亚线性分配也是如此,而不是像std::list 那样分配每个节点。
  • Fill 可能是std::generate_n(std::back_inserter(pool), size, [](){return std::make_unique&lt;Object&gt;(*obj);});
  • 专题教科书是学习、研究和理解这些核心算法和计算机科学主题的最佳资源。使用教科书作为学习指南,你肯定走在正确的轨道上。不要被“互联网上的几个例子”或自封专家声称的“最佳实践”分散注意力;或“被[那些]使用”其他东西的人“告诉”某事或其他事情。任何小丑都可以上传视频到 Youtube 胡言乱语;或在网站上发布一些愚蠢的东西。你应该忽略所有这些,只关注你的教科书。
  • 请注意,您的 returnToPool 实际上不起作用。 returningObject 需要作为参考传递。
  • @MooingDuck 哦,好的,我明白了......我看到使用 std::stack 的例子较少,但我值得深入研究它,谢谢你的信息!..

标签: c++ memory-management object-pooling


【解决方案1】:

如果你不介意滚动你自己的链表逻辑,你可以创建一个不使用任何数据结构的 ObjectPool 类,因此保证不会访问堆(除非它的空闲列表是空,它必须这样做才能返回一个新对象,当然)。像这样的东西(警告,几乎没有经过测试的代码,可能包含错误):

#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;
}

【讨论】:

  • 嘿,谢谢你的例子,杰里米,我会仔细研究一下
  • 使用继承和static_cast可能会更好。
  • 最初我确实使用了“class ObjectNode : public Object”,但是如果 Object 类被标记为“final”,那么这样做就行不通了。
  • 我认为您不需要双重链接。单链表会更简单,也同样有效。
  • 是的,我相信这是真的。不过,我将保留代码原样,因为此时我不想冒险添加错误。
猜你喜欢
  • 2012-11-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-26
  • 1970-01-01
  • 2010-12-03
相关资源
最近更新 更多