使用boost::shared_ptr时可能存在哪些潜在危险?

49
13个回答

43

循环引用:指向原始对象的shared_ptr<>的某些内容有shared_ptr<>。当然,您可以使用weak_ptr<>来打破这个循环。


以下是我在评论中提到的一个示例。

class node : public enable_shared_from_this<node> {
public :
    void set_parent(shared_ptr<node> parent) { parent_ = parent; }
    void add_child(shared_ptr<node> child) {
        children_.push_back(child);
        child->set_parent(shared_from_this());
    }

    void frob() {
        do_frob();
        if (parent_) parent_->frob();
    }

private :
    void do_frob();
    shared_ptr<node> parent_;
    vector< shared_ptr<node> > children_;
};
在这个例子中,你有一个节点树,每个节点都持有指向其父节点的指针。无论出于什么原因,frob()成员函数会通过树向上传播。(这并非完全荒谬; 一些GUI框架就是这样工作的)。
问题在于,如果你失去对最顶层节点的引用,那么最顶层节点仍然持有其子节点的强引用,而所有子节点也持有其父节点的强引用。这意味着存在循环引用,阻止所有实例自我清理,同时代码无法访问该树,这导致内存泄漏。
class node : public enable_shared_from_this<node> {
public :
    void set_parent(shared_ptr<node> parent) { parent_ = parent; }
    void add_child(shared_ptr<node> child) {
        children_.push_back(child);
        child->set_parent(shared_from_this());
    }

    void frob() {
        do_frob();
        shared_ptr<node> parent = parent_.lock(); // Note: parent_.lock()
        if (parent) parent->frob();
    }

private :
    void do_frob();
    weak_ptr<node> parent_; // Note: now a weak_ptr<>
    vector< shared_ptr<node> > children_;
};

这里,父节点已被一个弱指针替换。它不再掌控所引用的节点的生命周期。因此,如果像前面的例子那样顶层节点超出范围,虽然它持有其子节点的强引用,但子节点不持有父节点的强引用。因此,没有对象的强引用,它会自我清理。这又导致子节点失去它们唯一的强引用,从而导致它们自我清理,以此类推。简而言之,这不会泄漏。只需通过战略性地将 shared_ptr<> 替换为 weak_ptr<> 就可以实现。

注意:上述内容同样适用于 std::shared_ptr<> 和 std::weak_ptr<>, 以及 boost::shared_ptr<> 和 boost::weak_ptr<>。


2
@Phil,是的,这是我对设计弱点最反感的地方:在创建堆上的对象时不考虑数据所有权。在多线程项目中情况会变得更糟。 - unwesen
1
@DennisZickefoose 你是如何做到的呢?既然有一个循环,那么是什么让你能够选择一个强引用并将其变为弱引用呢? - curiousguy
2
@curiousguy 我不确切地知道你遇到的问题是什么。只需将一个 shared_ptr<> 替换为 weak_ptr<>,这样就可以打破循环依赖。如果你能详细阐述一下你遇到的问题,我很乐意为你改进我的回答。 - Kaz Dragon
2
@curious:有趣的是,我曾因为这个原因用乘法代替了除法。 “绝对胡说八道”和“过度简化”是两个截然不同的概念。 - Dennis Zickefoose
2
@curiousguy 我偶然看到了你在其他地方的另一个答案,我想我理解了你的观点。 CMIIW:你认为没有循环依赖,这可以从你可以在第一次放置weak_ptr中得到证明。我认为这并不是很有帮助——那么就应该得出结论:不存在循环依赖。对象都是独立构建的。因此,让我们用一种有用的方式来定义“循环依赖”,即“拥有相互依赖生命周期的对象”。我认为这是合理的。因此,我的答案是基于原始问题的。 - Kaz Dragon
显示剩余4条评论

26

创建多个不相关的指向同一对象的 shared_ptr:

#include <stdio.h>
#include "boost/shared_ptr.hpp"

class foo
{
public:
    foo() { printf( "foo()\n"); }

    ~foo() { printf( "~foo()\n"); }
};

typedef boost::shared_ptr<foo> pFoo_t;

void doSomething( pFoo_t p)
{
    printf( "doing something...\n");
}

void doSomethingElse( pFoo_t p)
{
    printf( "doing something else...\n");
}

int main() {
    foo* pFoo = new foo;

    doSomething( pFoo_t( pFoo));
    doSomethingElse( pFoo_t( pFoo));

    return 0;
}

7
当然,这里的解决方案是永远不要在shared_ptr构造函数之外使用new - rlbond
21
请参考boost::make_shared()以完全避免使用new操作符。 :) - Macke
@MichaelBurr 所以...在这种情况下,我们可以使用pFoo_t pFoo(new foo); 这种实现方式(示例)是一种不好的设计吗?- - Aditya P

18

在函数调用的参数中创建一个匿名临时共享指针:

f(shared_ptr<Foo>(new Foo()), g());

这是因为允许执行new Foo(),然后调用g(),并且g()抛出异常,而没有设置shared_ptr,所以shared_ptr没有机会清理Foo对象。


我同意这个原则,但是你知道有哪个编译器会按照这种奇怪的顺序执行吗?!?我使用g++进行了测试,顺序是“正确”的:1) new Foo() 2) shared_ptr 3) g() - skurton
@skurton 我不知道有哪个编译器会触发这个问题,但是shared_ptr构造函数和g()的执行顺序没有被C++标准规定,所以编译器可以随意排列它们。现在市场上有很多编译器,它们都有许多优化选项,并且针对你的代码进行优化可能涉及到许多因素;依赖未定义行为从来都不是一个好主意。我建议阅读Clang开发者关于未定义行为的系列文章:1, 2, 3 - Brian Campbell
我的意思是顺序A和B都可以:A: new Foo() / shared_ptr() / g() / f()。B: g() / new Foo() / shared_ptr() / f()。一个不好的顺序是:new Foo() / g() / shared_ptr() / f(),我认为这个不好的顺序不是标准的。 - skurton
1
@skurton 标准并未规定第三个顺序无效;因此,编译器可以自由选择该顺序。这里是Boost文档,描述了这个问题,这里有另一个解释。请参阅Herb Sutter的书《More Exceptional C++》以获取更多详细信息。请注意,在C++11中现在有一个std::make_shared(),它可以在单个调用中分配对象并将其包装在共享指针中,以避免此问题。 - Brian Campbell

13

小心将两个指针指向同一个对象。

boost::shared_ptr<Base> b( new Derived() );
{
  boost::shared_ptr<Derived> d( b.get() );
} // d goes out of scope here, deletes pointer

b->doSomething(); // crashes

请使用这个代替。
boost::shared_ptr<Base> b( new Derived() );
{
  boost::shared_ptr<Derived> d = 
    boost::dynamic_pointer_cast<Derived,Base>( b );
} // d goes out of scope here, refcount--

b->doSomething(); // no crash

另外,任何持有 shared_ptr 的类都应该定义复制构造函数和赋值运算符。

不要在构造函数中尝试使用 shared_from_this() -- 它不起作用。相反,创建一个静态方法来创建类并返回 shared_ptr。

我已经毫无问题地传递了 shared_ptr 的引用。只要确保在保存之前它被复制了(即,没有引用作为类成员)。


4
“此外,任何持有 shared_ptr 的类都应该定义拷贝构造函数和赋值运算符。” ----为什么?使用智能指针的目的不是减少对拷贝构造函数和赋值运算符的需求吗? - Andreas

12

以下是需要避免的两件事情:

  • 调用 get() 函数获取原始指针并在所指向的对象超出作用域后继续使用它。

  • 将引用或原始指针传递给 shared_ptr 也可能很危险,因为它不会增加内部计数,该计数有助于保持对象的生命。


1
然而,要意识到通过 get() 传递原始指针以调用旧例程是很常见的 - 但你必须知道你所调用的函数不会拥有该指针/引用(或在对象生命周期之外保留它)。因此,这并不理想,但这是你可能不得不做的事情。 - Michael Burr
希望不会太久,因为据我所知,shared_ptr 已经在 tr1 中了。(尽管我在我的 g++ 4.3.3 实现中没有找到它,除了一个不应该包含在用户代码中的“内部头文件”之外。) - Frank

10

我们调试了数周奇怪的行为。

原因是:
我们给一些线程工作者传递了 'this',而不是 'shared_from_this'。


4
并不是一个明显的错误,但在使用C++0x的方式之前,它肯定会让人感到沮丧:你所熟悉和喜欢的大多数谓词函数在处理shared_ptr时并不友好。幸运的是,std::tr1::mem_fn可以用于对象、指针和shared_ptr,取代了std::mem_fun。但如果你想要使用std::negate、std::not1、std::plus或任何那些老朋友与shared_ptr一起使用,就要准备好与std::tr1::bind亲密接触,并且可能还需要使用参数占位符。实际上,这其实更加通用,因为现在你基本上需要为每个函数对象适配器使用bind,但如果你已经熟悉STL的便利函数,那么这需要一些时间来适应。

这篇DDJ文章涉及到这个主题,并附有大量示例代码。几年前,当我第一次不得不弄清如何做时,我也写了一个博客


3

如果你在堆上有很多小对象,但它们并不真正“共享”,那么使用shared_ptr来处理像char short这样的非常小的对象可能会导致开销。在g++ 4.4.3和Boost 1.42中,boost::shared_ptr为每个新引用计数分配16字节。std::tr1::shared_ptr则分配20字节。现在,如果你有一百万个不同的shared_ptr<char>,那么仅保持count=1就会消耗2000万字节的内存。更不用说间接成本和内存碎片化了。试着在你最喜欢的平台上尝试以下内容。

void * operator new (size_t size) {
  std::cout << "size = " << size << std::endl;
  void *ptr = malloc(size);
  if(!ptr) throw std::bad_alloc();
  return ptr;
}
void operator delete (void *p) {
  free(p);
}

哦,是的。我很反感那些不注意内存使用或对其毫不在意的人。 - Zan Lynx
为什么会出现碎片化? - curiousguy

1
在类定义中传递shared_ptr 到this也是很危险的。 应该使用enabled_shared_from_this。 请参阅以下文章here

1

在多线程代码中使用shared_ptr时需要小心。很容易出现这样的情况,即不同线程使用指向相同内存的几个shared_ptr


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