使用shared_ptr和weak_ptr时避免间接循环引用问题

7
我正在开发一个应用程序,它严重依赖于shared_ptr,目前一切看起来都很好 - 我已经做了功课,对使用shared_ptr的一些陷阱有了相当清晰的认识。 shared_ptr最为人所知的问题之一是循环依赖 - 这些问题可以通过存储不影响上游对象生命周期的weak_ptr来解决。然而,我在理解需要通过weak_ptr存储指向外部对象的指针的时候遇到了困难 - 我不确定这是否被禁止、不鼓励还是安全的
下面的图表描述了我的意思(黑色箭头表示shared_ptr; 虚线表示weak_ptr):

alt text http://img694.imageshack.us/img694/6628/sharedweakptr.png

  • 一个父类包含指向两个子类的shared_ptr,两个子类使用weak_ptr指回父类。
  • 在第一个子类的构造函数中,我通过父类的weak_ptr检索到指向第二个子类的指针并将其存储在本地。

代码如下:

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <boost/make_shared.hpp>
#include <boost/enable_shared_from_this.hpp>

class child;
class child2;
class parent;

class parent : public boost::enable_shared_from_this<parent>
{
public:
    void createChildren()
    {
        _child2 = boost::make_shared<child2>(shared_from_this());
        _child = boost::make_shared<child>(shared_from_this());
    }

    boost::shared_ptr<child> _child;
    boost::shared_ptr<child2> _child2;
};

class child
{
public:
    child(boost::weak_ptr<parent> p)
    {
        _parent = p;
        _child2 = boost::shared_ptr<parent>(p)->_child2; // is this safe?
    }

    boost::weak_ptr<parent> _parent;
    boost::shared_ptr<child2> _child2;
};

class child2
{
public:
    child2(boost::weak_ptr<parent> p)
    {
        this->_parent = p;
    }

    boost::weak_ptr<parent> _parent;
};

int main()
{
    boost::shared_ptr<parent> master(boost::make_shared<parent>());
    master->createChildren();
}

我已经测试过了,它似乎可以正常工作(没有任何内存泄漏的报告),但是我的问题是:这是否安全?如果不安全,为什么?

可能是重复问题:https://dev59.com/oHI-5IYBdhLWcg3wfoa1 - Kirill V. Lyadvinsky
我正在使用weak_ptr来打破父子之间的循环引用。我的问题是关于存储通过父对象检索到的兄弟shared_ptr。 - Alan
2
我认为过度依赖shared_ptr并不是一个好主意。这是一种专门的工具,有特定的使用场景。在你的例子中,为什么不将子对象scoped_ptrs,而将父对象的句柄作为原始指针呢? - rpg
3个回答

6

在你调用子构造函数时,它似乎是安全的。但总体上并不安全。

问题出在将weak_ptr作为子构造函数的参数传递。这意味着你需要担心弱指针是否指向一个不存在的对象。通过将此参数更改为shared_ptrs,并在存储时转换为weak_ptr,我们知道该对象仍然存在。以下是更改:

child(boost::shared_ptr<parent> p)
{
    _parent = p;
    _child2 = p->_child2; // This is this safe
}

3
shared_ptr最常见的问题之一就是循环依赖,这些问题可以通过存储不影响上游对象生命周期的weak_ptr来解决。
错误。这种循环依赖要么存在,要么不存在。
如果存在这个问题,那么弱引用就不是一个选择。
只有在必须通过weak_ptr存储指向外部对象的指针时,才几乎不需要使用weak_ptr。
有些非常特殊的情况需要使用weak_ptr,但大多数情况下,它是shared_ptr的一部分:与其随意将shared_ptr应用于问题,不如随机抛出一半shared_ptr和一半weak_ptr(在SO上看到的)。

如果您有循环依赖关系,将链接更改为弱指针会改变依赖关系的性质,可能会消除问题。因此,即使存在其他选项,这肯定是一种选择。 - Dennis Zickefoose
1
如果存在循环依赖,您无法使用弱引用。如果可以使用弱引用,则证明您确实没有循环依赖:因为您可以应对一个链接可能被断开的事实,所以它不是依赖关系。如果您不需要某些东西,那么您就不依赖它。 “将链接更改为弱指针会改变依赖关系的性质”是的,它将依赖关系的性质更改为:不是依赖关系。这有所帮助。 - curiousguy
我看到你在几个关于shared pointers的问题上发表的抱怨,我在某种程度上是同意的。weak_ptr在一些情况下确实有帮助,但并没有解决总体问题。你现在还持有这种观点吗? - Matt Joiner
@MattJoiner 是的。通过用“弱引用”(又称“根本不是引用”)替换正常引用(即强引用)来“修复”循环依赖,就像在 42 个递归层之后退出来“修复”无限递归一样,最终你只会得到一个错误消息。 在这两种情况下,无能力的程序员关注症状而非问题。在进行无锁编程或使用递归互斥锁以避免死锁方面也有类似的故事。(在任何情况下,我并不“反对”弱引用或无锁习惯用法。但它们不是神奇的设备。) - curiousguy
有时候依赖关系在业务规则中,需要为用户提供明确的错误信息。 - Jean-Michaël Celerier

0
如果“p”已经被销毁,您将收到bad_weak_ptr异常。因此,只有在子构造函数期望异常时才是安全的,否则不安全。

2
在这段代码中,p 无法被有效地销毁,因为其父对象必须仍然存在:我们正在调用其成员函数。我认为子类构造函数应该接受一个 shared_ptr,使用它来恢复 child2 指针,但是存储一个指向父对象的 weak_ptr。或者可以采用两个参数的方式。如果您知道调用方拥有 shared_ptr,并且被调用方需要使用该值,则没有必要将其作为 weak_ptr 传递。如果其他地方的代码创建了子对象,并且只有 weak_ptr,则可能需要一个接受 weak_ptr 的构造函数。 - Steve Jessop
关于构造函数采用 shared_ptr 然后转换为 weak_ptr 进行存储的建议很好 - 我会认真考虑的(这肯定会使代码更加整洁)。 - Alan
顺便提一下,“take two parameters”是指一个给父对象,另一个给子对象2。重新阅读后,我注意到这可能被理解为一个shared_ptr和一个weak_ptr都给父对象,但那不是我的意思。 - Steve Jessop
2
@SteveJessop "由于父对象仍然必须存在",你一开始就不需要智能指针。使用常规指针是显而易见、自然、简单和高效的解决方案。 - curiousguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接