21

I'm having fun with c++-ideas, and got a little stuck with this problem.

I would like a LIFO class that manages a pool of resources. When a resource is requested (through acquire()), it returns the object as a unique_ptr that, upon deletion, causes the resource to be returned to the pool.

The unit tests would be:

// Create the pool, that holds (for simplicity, int objects)
SharedPool<int> pool;
TS_ASSERT(pool.empty());

// Add an object to the pool, which is now, no longer empty
pool.add(std::unique_ptr<int>(new int(42)));
TS_ASSERT(!pool.empty());

// Pop this object within its own scope, causing the pool to be empty
{
  auto v = pool.acquire();
  TS_ASSERT_EQUALS(*v, 42);
  TS_ASSERT(pool.empty());
}

// Object should now have returned to the pool
TS_ASSERT(!pool.empty())

Basic implementation, which would pass the tests, except for the important final test:

template <class T>
class SharedPool
{
 public:
  SharedPool(){}
  virtual ~SharedPool(){}

  void add(std::unique_ptr<T> t) {
    pool_.push(std::move(t));
  }

  std::unique_ptr<T> acquire() {
    assert(!pool_.empty());
    std::unique_ptr<T> tmp(std::move(pool_.top()));
    pool_.pop();
    return std::move(tmp);
  }

  bool empty() const {
    return pool_.empty();
  }

 private:
  std::stack<std::unique_ptr<T> > pool_;
};

The question: How to go about so that acquire() returns a unique_ptr of a type such that the deleter has knowledge of this, and calls this->add(...), returning the resource back to the pool.

swalog
  • 4,403
  • 3
  • 32
  • 60
  • 2
    If you are using a custom deleter, you are no longer returning a `std::unique_ptr`. Either fix the signature or use something with a type-erased deleter (such as `shared_ptr`). – T.C. Jan 07 '15 at 20:22
  • I know :), it might be of the type `std::unique_ptr >`, but I didn't want to add half-answers. My confusion is more how this is properly combined with `std::bind`. I'll rely on more experienced C++-devs to fill in the blanks. An alternative, which I wanted to tackle afterwards, was returning a `std::shared_ptr`, but, if it's properly solved for `std::unique_ptr`, it's automatically solved for the `shared_ptr` case. – swalog Jan 07 '15 at 20:24

4 Answers4

23

Naive implementation

The implementation uses unique_ptr with a custom deleter that returns objects to the pool. Both acquire and release are O(1). Additionally, unique_ptr with custom deleters can be implicitly converted to shared_ptr.

template <class T>
class SharedPool
{
 public:
  using ptr_type = std::unique_ptr<T, std::function<void(T*)> >;

  SharedPool() {}
  virtual ~SharedPool(){}

  void add(std::unique_ptr<T> t) {
    pool_.push(std::move(t));
  }

  ptr_type acquire() {
    assert(!pool_.empty());
    ptr_type tmp(pool_.top().release(),
                 [this](T* ptr) {
                   this->add(std::unique_ptr<T>(ptr));
                 });
    pool_.pop();
    return std::move(tmp);
  }

  bool empty() const {
    return pool_.empty();
  }

  size_t size() const {
    return pool_.size();
  }

 private:
  std::stack<std::unique_ptr<T> > pool_;
};

Example usage:

SharedPool<int> pool;
pool.add(std::unique_ptr<int>(new int(42)));
pool.add(std::unique_ptr<int>(new int(84)));
pool.add(std::unique_ptr<int>(new int(1024)));
pool.add(std::unique_ptr<int>(new int(1337)));

// Three ways to express the unique_ptr object
auto v1 = pool.acquire();
SharedPool<int>::ptr_type v2 = pool.acquire();    
std::unique_ptr<int, std::function<void(int*)> > v3 = pool.acquire();

// Implicitly converted shared_ptr with correct deleter
std::shared_ptr<int> v4 = pool.acquire();

// Note that adding an acquired object is (correctly) disallowed:
// pool.add(v1);  // compiler error

You might have caught a severe problem with this implementation. The following usage isn't unthinkable:

  std::unique_ptr< SharedPool<Widget> > pool( new SharedPool<Widget> );
  pool->add(std::unique_ptr<Widget>(new Widget(42)));
  pool->add(std::unique_ptr<Widget>(new Widget(84)));

  // [Widget,42] acquired(), and released from pool
  auto v1 = pool->acquire();

  // [Widget,84] is destroyed properly, together with pool
  pool.reset(nullptr);

  // [Widget,42] is not destroyed, pool no longer exists.
  v1.reset(nullptr);
  // Memory leak

We need a way to keep alive information necessary for the deleter to make the distinction

  1. Should I return object to pool?
  2. Should I delete the actual object?

One way of doing this (suggested by T.C.), is having each deleter keep a weak_ptr to shared_ptr member in SharedPool. This lets the deleter know if the pool has been destroyed.

Correct implementation:

template <class T>
class SharedPool
{
 private:
  struct External_Deleter {
    explicit External_Deleter(std::weak_ptr<SharedPool<T>* > pool)
        : pool_(pool) {}

    void operator()(T* ptr) {
      if (auto pool_ptr = pool_.lock()) {
        try {
          (*pool_ptr.get())->add(std::unique_ptr<T>{ptr});
          return;
        } catch(...) {}
      }
      std::default_delete<T>{}(ptr);
    }
   private:
    std::weak_ptr<SharedPool<T>* > pool_;
  };

 public:
  using ptr_type = std::unique_ptr<T, External_Deleter >;

  SharedPool() : this_ptr_(new SharedPool<T>*(this)) {}
  virtual ~SharedPool(){}

  void add(std::unique_ptr<T> t) {
    pool_.push(std::move(t));
  }

  ptr_type acquire() {
    assert(!pool_.empty());
    ptr_type tmp(pool_.top().release(),
                 External_Deleter{std::weak_ptr<SharedPool<T>*>{this_ptr_}});
    pool_.pop();
    return std::move(tmp);
  }

  bool empty() const {
    return pool_.empty();
  }

  size_t size() const {
    return pool_.size();
  }

 private:
  std::shared_ptr<SharedPool<T>* > this_ptr_;
  std::stack<std::unique_ptr<T> > pool_;
};
swalog
  • 4,403
  • 3
  • 32
  • 60
  • 3
    I'd store the objects in the pool as plain `unique_ptr`s (no custom deleter), and attach the custom deleter only in the `acquire()` (which can return either a `unique_ptr` with a custom deleter or a `shared_ptr` with a type-erased deleter). This also obviates the need to write a custom destructor. – T.C. Jan 09 '15 at 02:46
  • @T.C. You are absolutely right. I've updated my answer. – swalog Jan 09 '15 at 14:03
  • 1
    The constructor of `std::function` may throw, but `unique_ptr` requires that its deleter's constructors must not throw, so you might want to use a custom deleter type. Also, your "more robust" version essentially uses a reference-counted smart pointer. There is already such a pointer in the standard library... – T.C. Jan 09 '15 at 18:52
  • I haven't accepted my own answer, since there must be a better implementation. I don't see how using a `shared_ptr` can substitute the reference counting and also keep it simpler. I think the problem is that I use `unique_ptr` to represent items that leave the pool. Since they are still linked to an object whose lifetime is undetermined by the item, it should perhaps be a `shared_ptr`, that only becomes unique if from a deleted pool. But this also feels incorrect. As for the first issue, how would a custom deleter type solve that? Aren't both ultimately just static functions? – swalog Jan 11 '15 at 12:21
  • The pool has a `shared_ptr`, and the deleters can each carry a `weak_ptr` that they can use to determine if the pool remains valid. `std::function`'s constructor can throw because it does type erasure and must use dynamic allocation (to handle arbitrarily large functors), but if you use a custom deleter type then there's no erasure and hence no dynamic allocation to be done. – T.C. Jan 11 '15 at 15:58
  • @T.C. I've updated my answer to follow your suggestions as best as possible. I'm not sure I fully understood how (or if I was supposed to,) avoid `function` all together. Thanks a lot for your help and suggestions. – swalog Jan 13 '15 at 15:01
  • 2
    The custom deleter still needs to call `std::stack>::push(unique_ptr&&)` which can throw, and if that happens inside a `unique_ptr` destructor it will call `std::terminate()`, so you need to handle `bad_alloc` – Jonathan Wakely Jan 13 '15 at 15:21
  • Yes, I meant that you should avoid using `std::function`. Jonathan Wakely's answer, esp. the second version, is pretty much what I had in mind. – T.C. Jan 13 '15 at 17:12
  • Thanks T.C and Jonathan Wakely. I learned several things with this. I've updated my answer again to reflect the last suggestions. I did however not understand certain aspects of Jonathan Wakely's second suggestion (the `dummy, this` part), and kept my own, now updated, and hopefully finalm version. – swalog Jan 14 '15 at 11:41
  • And last but not least none of this is thread safe. At least add and acquire need locks in the current version, unless each thread has their own pool. Your `ptr_type` is also at least twice the size of a raw ptr - the actual ptr, the custom deleter, and then the weak_ptr. If you were to use the pool as a singleton, then this memory overhead would be quite wasteful, as all information except for the raw pointer would always be identical. Returning by shared_ptr will introduce more overhead as well - namely you'd need to go to the heap for every allocation, which defeats the point of the pool? – Cookie Feb 19 '17 at 20:39
  • 1
    I've learnt a lot from this answer. One thing that I still dont understand, what is meant by the deference operator (\*) in std::weak_ptr\* > pool_ – vibz Apr 04 '18 at 10:02
  • Why not do this in the deleter of the object owned by the std::unique_ptr? – ruipacheco Feb 12 '19 at 10:16
  • In my implementation, it seems that `External_Deleter` needs a trivial default constructor in order to create a `SharedPool::ptr_type` before assigning it. – RandyP Oct 29 '19 at 22:42
  • This is a really interesting problem. I think there are a number of use cases for a general construct like this, and this is the most complete starting point I've seen so far. The thread safety aspect can be addressed, I believe, with something like the concurrent_queue and the like from Intel's TBB - https://software.intel.com/en-us/node/506200 – aggieNick02 Jun 26 '20 at 02:55
  • @Cookie - shared_ptr makes sense for certain use cases. If you are dealing with objects in the pool that are extremely expensive to create, the overhead of the bytes to manage all of this is not significant. – aggieNick02 Jun 26 '20 at 02:57
  • 1
    I ended up combining several of the good ideas in this solution with parts of the `PoolableObjectFactory` from the Poco library (and using a thread-safe queue instead of a stack). It works for my needs really well. I also want to extend it to custom pointer types and deleters for the pooled objects, which is useful if you want a pool of something like Windows HANDLEs . For now I have to wrap such things in a real class. `unique_ptr`/`shared_ptr` allow for such things, but there is a bit of STL magic involved in how the pointer type is inferred. – aggieNick02 Jul 02 '20 at 21:01
11

Here's a custom deleter that checks if the pool is still alive.

template<typename T>
class return_to_pool
{
  std::weak_ptr<SharedPool<T>> pool

public:
  return_to_pool(const shared_ptr<SharedPool<T>>& sp) : pool(sp) { }

  void operator()(T* p) const
  {
    if (auto sp = pool.lock())
    {
      try {
        sp->add(std::unique_ptr<T>(p));
        return;
      } catch (const std::bad_alloc&) {
      }
    }
    std::default_delete<T>{}(p);
  }
};

template <class T>
class SharedPool : std::enable_shared_from_this<SharedPool<T>>
{
public:
  using ptr_type = std::unique_ptr<T, return_to_pool<T>>;
  ...
  ptr_type acquire()
  {
    if (pool_.empty())
      throw std::logic_error("pool closed");
    ptr_type tmp{pool_.top().release(), this->shared_from_this()};
    pool_.pop();
    return tmp;
  }
  ...
};

// SharedPool must be owned by a shared_ptr for enable_shared_from_this to work
auto pool = std::make_shared<SharedPool<int>>();
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • Would it be possible to explain the `std::shared_ptr{shared, this}`. It would be equivalent to the more explicit `return_to_pool{std::shared_ptr{shared, this}}`. But, why does this compile? Why does the `return_to_pool` constructor parameter accept a `shared_ptr` with `SharedPool::dummy`, and `SharedPool*` arguments. What is going on here? – swalog Jan 14 '15 at 10:53
  • 1
    @swalog http://stackoverflow.com/questions/25920681/what-is-the-difference-between-an-empty-and-a-null-stdshared-ptr-in-c/25920775#25920775 – T.C. Jan 14 '15 at 22:33
  • I don't understand your use of `shared`. What prevents `SharedPool` from going out of life (before `dummy` goes out of life) before the last `ptr_type` pointer was destroyed? It seems to me that the `weak_ptr` could tell you "it's still alife", while it may be not. Usually the aliasing constructor is passed a pointer to a subobject, and a shared_ptr to the surrounding object. But in your case it's kindof the other way around, which confuses me. – Johannes Schaub - litb Oct 03 '16 at 15:58
  • @litb, you're right it doesn't help with object lifetimes. IIRC I was thinking of cases where your pool is already managed elsewhere (e.g. as a global) and is guaranteed to outlive the deleters referring to it ... but in that case there's no advantage to using `weak_ptr` to refer to it. That's fragile code though, so it's not a good example. I'll remove it. – Jonathan Wakely Oct 04 '16 at 09:47
  • I want `acquire()` to return `nullptr` if pool is empty, but it seems not possible here, since custom deleter is used. I could use a `try { } catch {}` block, but I think there must be some performance decrease to some extent. Is there a better way to do this? – zhy Jul 21 '22 at 07:01
  • @zhy, won't that just work? If you return an empty `ptr_type` then the deleter won't get invoked, but that's fine because there's nothing to return to the pool. – Jonathan Wakely Jul 22 '22 at 09:47
  • @JonathanWakely I finally fixed my error. I put a `explicit` specifier before class `return_to_pool `, which prevented instantiation of `ptr_type`. – zhy Aug 03 '22 at 14:12
5

Although the question is old and has already been answered, I have one minor comment on the solution proposed by @swalog.

Deleter functor can result in memory corruption due to double deletion:

void operator()(T* ptr) {
  if (auto pool_ptr = pool_.lock()) {
    try {
      (*pool_ptr.get())->add(std::unique_ptr<T>{ptr});
      return;
    } catch(...) {}
  }
  std::default_delete<T>{}(ptr);
}

unique_ptr created here will be destroyed when exception is caught. Hence,

std::default_delete<T>{}(ptr);

will result in double deletion.

It can be fixed by changing a place of creating unique_ptr from T*:

void operator()(T* ptr) {
  std::unique_ptr<T> uptr(ptr);
  if (auto pool_ptr = pool_.lock()) {
    try {
      (*pool_ptr.get())->add(std::move(uptr));
      return;
    } catch(...) {}
  }
}
fgg
  • 51
  • 1
  • 2
2

Consider using a shared_ptr instead. The only change you'd have to make is to not count auto pointers with more than one owner. Objects that had would aquired from the SharedPool could delete the auto pointer as normal, but the SharedPool would still hold the actual auto pointer.

template <class T>
class SharedPool
{
 public:
  SharedPool(){}
  virtual ~SharedPool(){}

  void add(std::unique_ptr<T> t) {
    pool_.push_back(std::move(t));
  }

  std::shared_ptr<T> acquire() {
    assert(!empty());
    return *std::find_if(pool_.begin(), pool.end(), [](const std::shared_ptr<T>& i){return i.count() == 1;});
  }

  bool empty() const {
    return std::none_of(pool_.begin(), pool_.end(), [](const std::shared_ptr<T>& i){return i.count() == 1;});
  }

 private:
  std::vector<std::shared_ptr<T>> pool_;
};
Jonathan Mee
  • 37,899
  • 23
  • 129
  • 288
  • Thanks for this suggestion. The only downside (but important one), is that `acquire` and `empty` are `O(n)`, when they should be `O(1)` (since they can). – swalog Jan 08 '15 at 09:06
  • I've added an answer with the `O(1)` implementation I was thinking of. – swalog Jan 08 '15 at 10:32
  • Instantiating `shared_ptr` means extra memory on the heap and hence extra malloc for reference counter. – Jacek Tomaka Nov 12 '21 at 05:04