24

shared_ptr 是 Boost 库中的引用计数智能指针。

引用计数的问题是它不能处理循环。我想知道如何在 C++ 中解决这个问题。

请不要提出诸如“不要循环”或“使用弱点”之类的建议。

编辑

我不喜欢只使用 weak_ptr 的建议,因为显然如果你知道你会创建一个循环,那么你就不会有问题。如果您在运行时生成 shared_ptrs,您也无法知道在编译时会有一个循环。

因此,请自行删除其中使用 weak_ptr 的答案,因为我特别要求不要有那种答案......

4

12 回答 12

28

shared_ptr表示所有权关系。而weak_ptr代表意识。拥有多个相互拥有的对象意味着您在架构方面存在问题,这可以通过将一个或多个自己的's 更改为意识到's(即weak_ptr's)来解决。

我不明白为什么建议weak_ptr被认为是无用的。

于 2009-04-22T11:49:06.033 回答
21

我理解您对被轻率地告知使用weak_ptr来破坏循环引用和我自己的烦恼,当我被告知循环引用是不好的编程风格时,我几乎感到愤怒。

你特别问你如何发现循环引用。事实是,在一个复杂的项目中,一些参考周期是间接的并且难以发现。

答案是你不应该做出让你容易受到循环引用的错误声明。我是认真的,我在批评一种非常流行的做法——盲目地对所有事情使用 shared_ptr。

您应该在设计中清楚哪些指针是所有者,哪些是观察者。

供业主使用shared_ptr

供观察者使用weak_ptr——所有这些,而不仅仅是那些你认为可能是一个循环的一部分。

如果您遵循这种做法,那么循环引用不会引起任何问题,您无需担心它们。当然,当您想使用它们时,您将需要编写大量代码来将所有这些weak_ptrs 转换为s - Boost 确实无法胜任这项工作。shared_ptr

于 2009-08-07T08:38:05.530 回答
5

检测周期相当容易:

  • 将计数设置为一些较大的数字,例如 1000(确切大小取决于您的应用程序)
  • 从您感兴趣的先驱者开始,并遵循它的指示
  • 对于您遵循的每个指针,减少计数
  • 如果在到达指针链的末端之前计数下降到零,则有一个循环

然而,它不是很有用。而且通常不可能解决引用计数指针的循环问题——这就是为什么发明了替代垃圾收集方案(如生成清除)的原因。

于 2009-04-22T07:51:43.937 回答
4

我还没有找到比绘制大型 UML 图和寻找周期更好的方法。

为了调试,我使用一个实例计数器进入注册表,如下所示:

template <DWORD id>
class CDbgInstCount
{
public:
#ifdef _DEBUG
   CDbgInstCount()   { reghelper.Add(id, 1); }
   CDbgInstCount(CDbgInstCount const &) {  reghelper.Add(id, 1); }
   ~CDbgInstCount()  { reghelper.Add(id, -1); }
#else
#endif
};

我只是需要将它添加到有问题的类中,然后查看注册表。

(ID,如果给出例如 'XYZ!' 将被转换为字符串。不幸的是,您不能将字符串常量指定为模板参数)

于 2009-04-22T07:55:19.340 回答
2

boost::weak_ptr和可能的组合boost::shared_ptr这篇文章可能很有趣。

于 2009-04-22T08:27:28.467 回答
1

请参阅这篇关于在图中检测周期的帖子。

于 2009-04-22T14:09:51.873 回答
1

找到循环的通用解决方案可以在这里找到:

测试链表是否有循环的最佳算法

这假设您知道列表中对象的结构,并且可以跟踪每个对象中包含的所有指针。

于 2009-04-23T04:53:54.573 回答
1

您可能需要像Mark and Sweep这样的垃圾收集器技术。该算法的思想是:

  1. 保留一个列表,其中包含对分配的所有内存块的引用。
  2. 在某些时候你启动垃圾收集器:
    1. 它首先标记它仍然可以在不使用引用列表的情况下访问的所有块。
    2. 它通过列表擦除每个无法标记的项目,这意味着它不再可访问,因此它没有用。

由于您正在使用shared_ptr任何仍然存在的指针,因此您无法到达应该被视为循环的成员。

执行

下面我描述一个非常幼稚的例子,说明如何实现sweep()部分算法,但是它将reset()所有剩余的指针放在收集器上。

此代码存储shared_ptr<Cycle_t>指针。该类Collector负责跟踪所有指针并在sweep()执行时将其删除。

#include <vector>
#include <memory>

class Cycle_t;
typedef std::shared_ptr<Cycle_t> Ref_t;

// struct Cycle;
struct Cycle_t {
  Ref_t cycle;

  Cycle_t() {}
  Cycle_t(Ref_t cycle) : cycle(cycle) {}
};

struct collector {
  // Note this vector will grow endlessy.
  // You should find a way to reuse old links
  std::vector<std::weak_ptr<Cycle_t>> memory;

  // Allocate a shared pointer keeping
  // a weak ref on the memory vector:
  inline Ref_t add(Ref_t ref) {
    memory.emplace_back(ref);
    return ref;
  }
  inline Ref_t add(Cycle_t value) {
    Ref_t ref = std::make_shared<Cycle_t>(value);
    return add(ref);
  }
  inline Ref_t add() {
    Ref_t ref = std::make_shared<Cycle_t>();
    return add(ref);
  }

  void sweep() {
    // Run a sweep algorithm:
    for (auto& ref : memory) {
      // If the original shared_ptr still exists:
      if (auto ptr = ref.lock()) {
        // Reset each pointer contained within it:
        ptr->cycle.reset();

        // Doing this will trigger a deallocation cascade, since
        // the pointer it used to reference will now lose its
        // last reference and be deleted by the reference counting
        // system.
        //
        // The `ptr` pointer will not be deletd on the cascade
        // because we still have at least the current reference
        // to it.
      }
      // When we leave the loop `ptr` loses its last reference
      // and should be deleted.
    }
  }
};

然后你可以像这样使用它:

Collector collector;

int main() {
  // Build your shared pointers:
  {
    // Allocate them using the collector:
    Ref_t c1 = collector.add();
    Ref_t c2 = collector.add(c1);

    // Then create the cycle:
    c1.get()->cycle = c2;

    // A normal block with no cycles:
    Ref_t c3 = collector.add();
  }

  // In another scope:
  {
    // Note: if you run sweep an you still have an existing
    // reference to one of the pointers in the collector
    // you will lose it since it will be reset().
    collector.sweep();
  }
}

我用 Valgrind 对其进行了测试,没有列出内存泄漏或“仍然可访问”的块,因此它可能按预期工作。

关于此实现的一些注意事项:

  1. 内存向量会无限增长,你应该使用一些内存分配技术来确保它不会占用你所有的工作内存。
  2. 有人可能会争辩说,没有必要使用shared_ptr(其工作方式类似于引用计数 GC)来实现这样的垃圾收集器,因为标记和清除算法已经完成了这项工作。
  3. 我没有实现 mark() 函数,因为它会使示例复杂化,但这是可能的,我将在下面解释。

最后,如果您关心(2),这种实现并非闻所未闻。CPython(Python 的主要实现)确实使用了引用计数和标记和扫描的混合,但主要是出于历史原因

实现mark()功能:

要实现该mark()功能,您需要进行一些修改:

需要向 中添加一个bool marked;属性Cycle_t,并使用它来检查指针是否被标记。

您将需要编写如下所示的Collector::mark()函数:

void mark(Ref_t root) {
  root->marked = true;

  // For each other Ref_t stored on root:
  for (Ref_t& item : root) {
    mark(item);
  }
}

然后你应该修改sweep()函数以删除标记,如果指针被标记,否则reset()指针:

void sweep() {
  // Run a sweep algorithm:
  for (auto& ref : memory) {
    // If it still exists:
    if (auto ptr = ref.lock()) {
      // And is marked:
      if (ptr->marked) {
        ptr->marked = false;
      } else {
        ptr->cycle.reset();
      }
    }
  }
}

这是一个冗长的解释,但我希望它对某人有所帮助。

于 2017-05-26T18:20:46.173 回答
0

回答老问题,您可以尝试使用侵入式指针,这可能有助于计算资源被引用的次数。

#include <cstdlib>
#include <iostream>

#include <boost/intrusive_ptr.hpp>

class some_resource
{
    size_t m_counter;

public:
    some_resource(void) :
        m_counter(0)
    {
        std::cout << "Resource created" << std::endl;
    }

    ~some_resource(void)
    {
        std::cout << "Resource destroyed" << std::endl;
    }

    size_t refcnt(void)
    {
        return m_counter;
    }

    void ref(void)
    {
        m_counter++;
    }

    void unref(void)
    {
        m_counter--;
    }
};

void
intrusive_ptr_add_ref(some_resource* r)
{
    r->ref();
    std::cout << "Resource referenced: " << r->refcnt()
              << std::endl;
}

void
intrusive_ptr_release(some_resource* r)
{
    r->unref();
    std::cout << "Resource unreferenced: " << r->refcnt()
              << std::endl;
    if (r->refcnt() == 0)
        delete r;
}

int main(void)
{
    boost::intrusive_ptr<some_resource> r(new some_resource);
    boost::intrusive_ptr<some_resource> r2(r);

    std::cout << "Program exiting" << std::endl;

    return EXIT_SUCCESS;
}

这是返回的结果。

Resource created 
Resource referenced: 1 
Resource referenced: 2 
Program exiting 
Resource unreferenced: 1
Resource unreferenced: 0 
Resource destroyed
*** Program Exit ***
于 2013-05-04T19:31:52.960 回答
0

如果你有带有共享指针的 Cycles,你将会有内存泄漏。因此,如果您转储内存泄漏对象,您可以查看这些类型以找到实际循环。

于 2021-09-20T03:03:25.413 回答
0

我认为您确实在要求诸如 Java 的垃圾收集之类的东西这个问题讨论了一个“自动循环断路器” shared_ptr

可以在你的程序中有shared_ptr循环并让每个对象释放,但这违背了流行的建议。流行的建议是通过使用参与循环的对象之一来打破循环。shared_ptrweak_ptr

如果你坚持shared_ptr在你的程序中保留一个循环,你仍然可以这样做,但你必须在销毁时手动打破循环。shared_ptr

这很像记住手动调用delete一个对象,所以你可以看到为什么不推荐它。

struct B;

struct A {
    shared_ptr<B> b;
    void prepForShutdown() {
        b = nullptr; // unlink from b.
    }
    ~A() { puts("~A"); }
};

struct B {
    shared_ptr<A> a;
    ~B() { puts("~B"); }
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->b = b;
    b->a = a;

    a->prepForShutdown();  // Break the cycle
    // Without this, either dtor cannot run, because A holds a reference 
    // to b and B holds a reference to A.

    a = nullptr;
    b = nullptr;

}
于 2021-10-20T14:28:08.407 回答
-1

我知道你说“没有weak_ptr”,但为什么不呢?让你的头与一个weak_ptr 到尾巴,以及与一个weak_ptr 到头的尾巴将防止循环。

于 2009-04-22T14:05:21.150 回答