2

Is it possible to create an Object Pool of shared_ptr? Sketching this in my head, I can see two ways of doing this but each have a flaw:

  1. If T objects were stored in a reusable pool, the act of wrapping T in a shared_ptr on a get() request would result in the control block being re-allocated on the heap each time - therefore breaking the concept of an Object Pool.

  2. If shared_ptr objects were stored in a reusable pool, the shared_ptr object must stop existing to initiate a custom deleter, and the custom deleter function only gets called with a T pointer. So there is nothing to recycle.

Nicholas
  • 1,392
  • 16
  • 38

3 Answers3

5

After exhaustive research and testing I have concluded that there is no legitimate way (as of C++11 or below) to make an object pool of reusable shared_ptr<T>'s directly. Certainly one can make a pool of T objects quite easily that serves shared_ptr<T>'s, but that results in heap allocation with every serve for the control block.

It is possible however to make an object pool of shared_ptr<T>'s indirectly (and this is the only way I have found to do it). By indirectly, I mean one must implement a custom 'memory pool' style allocator to store for reuse the memory released when shared_ptr<T> control blocks are destroyed. This allocator is then used as the third parameter of the `shared_ptr' constructor:

template< class Y, class Deleter, class Alloc > 
   std::shared_ptr( Y* ptr, Deleter d, Alloc alloc );

The shared_ptr<T> will still be constructed/allocated and deleted/de-allocated with heap memory - there is no way to stop it - but by making the memory reusable through the custom allocator, a deterministic memory footprint can be achieved.

Nicholas
  • 1,392
  • 16
  • 38
  • `boost` offers such a memory pool custom allocator in its collection (if you are brave enough to go in that direction) ;-) – Nicholas Jul 22 '15 at 09:25
1

Yes, it's possible. But rather than making your pool return std::shared_ptr<T>, I would consider making it return boost::intrusive_ptr<T>. You could have intrusive_ptr_release() be responsible for freeing that block from the pool, and then it's just up to your users to construct T such that you can make an intrusive_ptr<T>.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    Sorry, I didn't mention that I'm not using boost - just C++11. – Nicholas Jun 27 '15 at 15:27
  • You should be able to use a custom deleter to return the object to the pool. No need for boost in that case. Also have a look at std::allocate_shared if using a pool allocator would suite your need. – Finn Jun 27 '15 at 22:05
  • @Finn, as I understand things a custom deleter cannot be specified with either make_shared or allocate_shared. – Nicholas Jun 28 '15 at 00:35
1

You can store the objects in a pool (e.g. as unique_ptr). The pool returns a shared_ptr on request. The custom deleter returns the data to the pool. A simple example of this is sketched here:

#ifndef __POOL_H_
#define __POOL_H_

#include <list>
#include <mutex>
#include <algorithm>
#include <memory>
#include <stdexcept>

namespace common {

template<class T, bool grow_on_demand=true>
class Pool 
{
    public:
    Pool(const char* name_p, size_t n) 
        : mutex_m(), free_m(0), used_m(0), name_m(name_p) 
    {
        for (size_t i=0; i<n; i++)
        {
           free_m.push_front( std::make_unique<T>() );
        }
    }

    const char* getName() const
    {
        return name_m.c_str();
    }

    std::shared_ptr<T> alloc()
    {
        std::unique_lock<std::mutex> lock(mutex_m);
        if (free_m.empty() )
        {
            if constexpr (grow_on_demand) 
            {
                free_m.push_front( std::make_unique<T>() );
            }
            else
            {
                throw std::bad_alloc();
            }
        }
        auto it = free_m.begin();
        std::shared_ptr<T> sptr( it->get(), [=](T* ptr){ this->free(ptr); } );
        used_m.push_front(std::move(*it));
        free_m.erase(it);
        return sptr;
    }


    size_t getFreeCount()
    {
        std::unique_lock<std::mutex> lock(mutex_m);
        return free_m.size();
    }

private:

    void free(T *obj)
    {
        std::unique_lock<std::mutex> lock(mutex_m);
        auto it = std::find_if(used_m.begin(), used_m.end(), [&](std::unique_ptr<T> &p){ return p.get()==obj; } );
        if (it != used_m.end())
        {
          free_m.push_back(std::move(*it));
          used_m.erase(it);
        }
        else
        {
            throw std::runtime_error("unexpected: unknown object freed.");
        }
    }

    std::mutex mutex_m;
    std::list<std::unique_ptr<T>> free_m;
    std::list<std::unique_ptr<T>> used_m;
    std::string name_m;
};

}

#endif /* __POOL_H_ */

By default, the pool adds new items if you allocate a new object from an empty pool (grow_on_demand=true).

  • A new Pool creates n elements and adds them to the Pool (using the default constructor).
  • With mypool.alloc() you can get an object from the pool.
  • When the allocated object is not used any more, the shared_ptr is returned to the pool automatically (happens implicitly through [=](T* ptr){ this->free(ptr); } from within alloc().
goto40
  • 76
  • 3