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.
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
- Should I return object to pool?
- 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_;
};
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>>();
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(...) {}
}
}
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_;
};
来源:https://stackoverflow.com/questions/27827923/c-object-pool-that-provides-items-as-smart-pointers-that-are-returned-to-pool