【问题标题】:C++ object-pool that provides items as smart-pointers that are returned to pool upon deletionC++ 对象池,将项目作为智能指针提供,在删除时返回池
【发布时间】:2015-03-05 20:20:15
【问题描述】:

我对 c++-ideas 很感兴趣,但遇到了这个问题。

我想要一个管理资源池的LIFO 类。 当请求资源时(通过acquire()),它会以unique_ptr 的形式返回对象,删除后会导致资源返回到池中。

单元测试是:

// 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())

基本实现,将通过测试,除了重要的最终测试:

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

问题:如何进行以使acquire()返回一个unique_ptr,其类型使得删除器知道this,并调用this-&gt;add(...),返回资源回到游泳池。

【问题讨论】:

  • 如果您使用自定义删除器,则不再返回 std::unique_ptr&lt;T&gt;。要么修复签名,要么使用带有类型擦除删除器的东西(例如shared_ptr)。
  • 我知道 :),它可能是 std::unique_ptr&lt;T, std::function&lt;void(T*)&gt; &gt; 类型,但我不想添加半答案。我的困惑更多是如何将其与std::bind 正确结合。我将依靠更有经验的 C++ 开发人员来填补空白。之后我想解决的另一种方法是返回一个std::shared_ptr,但是,如果它正确地解决了std::unique_ptr,它会自动解决shared_ptr 的情况。

标签: c++ c++11 smart-pointers pool


【解决方案1】:

朴素的实现

实现使用unique_ptr 和自定义删除器,将对象返回到池中。 acquirerelease 都是 O(1)。此外,带有自定义删除器的unique_ptr 可以隐式转换为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_;
};

使用示例:

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

您可能已经发现此实现存在严重问题。以下用法并非不可想象:

  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

我们需要一种方法来保持删除器进行区分所需的信息

  1. 我应该将对象返回到池中吗?
  2. 我应该删除实际对象吗?

这样做的一种方法(由 T.C. 建议)是让每个删除者在 SharedPool 中保留一个 weak_ptrshared_ptr 成员。这让删除者知道池是否已被销毁。

正确的实现:

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

【讨论】:

  • 我会将池中的对象存储为纯 unique_ptr&lt;T&gt;s(无自定义删除器),并仅在 acquire() 中附加自定义删除器(可以返回 unique_ptr 与自定义删除器或带有类型擦除删除器的shared_ptr)。这也避免了编写自定义析构函数的需要。
  • std::function 的构造函数可能会抛出,但 unique_ptr 要求其删除器的构造函数不能抛出,因此您可能需要使用自定义删除器类型。此外,您的“更强大”版本本质上使用引用计数智能指针。标准库里已经有这样的指针了……
  • 自定义删除器仍然需要调用std::stack&lt;unique_ptr&lt;T&gt;&gt;::push(unique_ptr&lt;T&gt;&amp;&amp;),它可以抛出,如果这发生在unique_ptr析构函数中,它将调用std::terminate(),所以你需要处理bad_alloc
  • 我从这个答案中学到了很多东西。我仍然不明白的一件事是 std::weak_ptr* > pool_ 中的尊重运算符 (*) 是什么意思
  • 我最终将这个解决方案中的几个好主意与 Poco 库中的 PoolableObjectFactory 的部分内容结合起来(并使用线程安全队列而不是堆栈)。它非常适合我的需求。我还想将它扩展到池对象的自定义指针类型和删除器,如果你想要一个像 Windows HANDLEs 这样的池,这很有用。现在我必须将这些东西包装在一个真正的类中。 unique_ptr/shared_ptr 允许这样的事情,但是在如何推断指针类型方面涉及到一点 STL 魔法。
【解决方案2】:

这是一个自定义删除器,用于检查池是否仍然存在。

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>>();

【讨论】:

  • 能否解释一下std::shared_ptr&lt;SharedPool&gt;{shared, this}。它相当于更明确的return_to_pool{std::shared_ptr&lt;SharedPool&gt;{shared, this}}。但是,为什么会编译?为什么return_to_pool 构造函数参数接受带有SharedPool&lt;T&gt;::dummySharedPool&lt;T&gt;* 参数的shared_ptr。这是怎么回事?
  • 我不明白你对shared的使用。在最后一个 ptr_type 指针被销毁之前,是什么阻止了 SharedPool 退出生命(在 dummy 退出生命之前)?在我看来,weak_ptr 可以告诉你“它还活着”,虽然它可能不是。通常,别名构造函数被传递一个指向子对象的指针,以及一个指向周围对象的 shared_ptr。但在你的情况下,它有点相反,这让我感到困惑。
  • @litb,你说得对,它对对象生命周期没有帮助。 IIRC 我正在考虑您的池已经在其他地方管理(例如作为全局)并且保证比引用它的删除器更长寿的情况......但在这种情况下,使用 weak_ptr 来引用它没有任何优势。虽然这是脆弱的代码,所以它不是一个很好的例子。我会删除它。
【解决方案3】:

虽然这个问题很老并且已经得到解答,但我对@swalog 提出的解决方案有一点小小的评论。

Deleter functor 可能会因双重删除而导致内存损坏:

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 会在捕获到异常时被销毁。 因此,

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

将导致双重删除。

可以通过更改从 T* 创建 unique_ptr 的位置来修复:

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(...) {}
  }
}

【讨论】:

    【解决方案4】:

    考虑改用shared_ptr。您必须做出的唯一更改是计算拥有多个所有者的自动指针。从SharedPool 获取的对象可以正常删除自动指针,但SharedPool 仍会保存实际的自动指针。

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

    【讨论】:

    • 感谢您的建议。唯一的缺点(但很重要)是 acquireemptyO(n),而它们应该是 O(1)(因为它们可以)。
    • 我已经用我正在考虑的O(1) 实现添加了一个答案。
    • 实例化 shared_ptr 意味着堆上有额外的内存,因此引用计数器需要额外的 malloc。
    猜你喜欢
    • 2012-09-10
    • 1970-01-01
    • 2015-07-02
    • 1970-01-01
    • 2014-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多