使用boost :: shared_ptr时有什么潜在的危险?

时间:2009-03-31 15:05:57

标签: c++ boost pointers shared-ptr

使用boost::shared_ptr时,有什么方法可以拍脚?换句话说,当我使用boost::shared_ptr时,我必须避免哪些陷阱?

13 个答案:

答案 0 :(得分:42)

循环引用: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&lt;&gt;使用weak_ptr&lt;&gt;。

注意:以上同样适用于std :: shared_ptr&lt;&gt;和std :: weak_ptr&lt;&gt;就像它的提升:: shared_ptr&lt;&gt;和boost :: weak_ptr&lt;&gt;。

答案 1 :(得分:24)

为同一个对象创建多个不相关的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;
}

答案 2 :(得分:18)

构造一个匿名临时共享指针,例如在函数调用的参数内:

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

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

答案 3 :(得分: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_ptrs的类都应该定义复制构造函数和赋值运算符。

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

我已经毫无困难地传递了对shared_ptrs的引用。只需确保在保存之前复制它(即没有作为类成员的引用)。

答案 4 :(得分:12)

这有两件事要避免:

  • 调用get()函数获取原始指针,并在指向对象超出范围后使用它。

  • 将引用或原始指针传递给shared_ptr也应该是危险的,因为它不会增加内部计数,这有助于保持对象的活着。

答案 5 :(得分:9)

我们调试了几个星期的奇怪行为。

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

答案 6 :(得分:4)

不完全是一个步枪,但肯定是令人沮丧的根源,直到你用C ++ 0x的方式探讨如何做到这一点:你知道并且喜欢来自<functional>的大部分谓词都不能很好地发挥作用与shared_ptr。令人高兴的是,std::tr1::mem_fn适用于对象,指针和shared_ptr,取代std::mem_fun,但如果您想使用std::negatestd::not1std::plus或者shared_ptr的任何一个老朋友,都准备好与std::tr1::bind和可能的论点占位符保持一致。在实践中,这实际上更通用,因为现在你基本上最终使用bind用于每个函数对象适配器,但如果你已经熟悉了STL的便利功能,它确实需要习惯。

This DDJ article触及了这个主题,有很多示例代码。几年前,当我第一次弄清楚如何去做时,我也blogged了解它。

答案 7 :(得分:3)

如果你在堆上有很多小对象但是它们并没有真正“共享”,那么对于真正的小对象(如shared_ptr char)使用short可能是一个开销。 boost::shared_ptr为它在g ++ 4.4.3和VS2008上创建的每个新引用计数分配16个字节,其中Boost为1.42。 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);
}

答案 8 :(得分:1)

发出shared_ptr&lt; T>在这个里面定义一个类定义也很危险。 请改用enabled_shared_from_this。

请参阅以下帖子here

答案 9 :(得分:1)

在多线程代码中使用shared_ptr时需要小心。然后,当不同的线程使用指向同一内存的几个shared_ptr时,它变得相对容易。

答案 10 :(得分:0)

shared_ptr的广泛使用几乎不可避免地会导致不必要的和看不见的内存占用。

循环引用是一个众所周知的原因,其中一些可能是间接的,很难发现,特别是在由多个程序员处理的复杂代码中;程序员可能决定一个对象需要引用另一个对象作为快速修复,并且没有时间检查所有代码以查看他是否正在关闭一个循环。这种危险被严重低估了。

不太清楚的是未发布的引用问题。如果一个对象被共享给许多shared_ptrs,那么在它们中的每一个被归零或超出范围之前它都不会被销毁。很容易忽略其中一个引用,并最终得到你认为已经完成的内存中看不见的对象。

虽然严格来说这些不是内存泄漏(它会在程序退出之前全部释放)但它们同样有害且难以检测。

这些问题是权宜的虚假声明的结果:1。声明你真正希望成为shared_ptr的单一所有权。 scoped_ptr是正确的,但是对该对象的任何其他引用都必须是一个原始指针,可以留下悬空。 2.声明你真正想要成为被动观察引用的是shared_ptr。 weak_ptr是正确的,但是每次你想要使用它时,你都有把它转换成share_ptr的麻烦。

我怀疑你的项目是这种做法可以帮助你解决的一个很好的例子。

如果你有一个内存密集型应用程序,你真的需要单一所有权,这样你的设计就可以明确地控制对象的生命周期。

单一所有权opObject = NULL;肯定会删除该对象,现在就可以了。

使用共享所有权spObject = NULL; ........谁知道?......

答案 11 :(得分:-1)

如果您有共享对象的注册表(例如,所有活动实例的列表),则永远不会释放对象。解决方案:与循环依赖结构的情况一样(参见Kaz Dragon的回答),请根据需要使用weak_ptr。

答案 12 :(得分:-5)

智能指针不适用于所有内容,原始指针无法消除

可能最大的危险是,由于shared_ptr是一个有用的工具,人们会开始把它放在每个地方。由于普通指针可能被滥用,同样的人会搜索原始指针并尝试用字符串,容器或智能指针替换它们,即使它没有意义。原始指针的合法使用将变得可疑。警察会有一个指针。

这不仅可能是最严重的危险,也可能是唯一的严重危险。 shared_ptr的所有最严重的滥用都是智能指针优于原始指针(无论这意味着什么)的直接结果,并且将智能指针放在各处将使C ++编程“更安全”。

当然,智能指针需要转换为要使用的原始指针这一事实驳斥了智能指针崇拜的这种说法,但原始指针访问在operator*中是“隐含的”这一事实,operator->(或get()中的显式),但不隐含在隐式转换中,足以让人觉得这不是真正的转换,并且这个非转换产生的原始指针是一个无害的临时。

C ++不能成为“安全语言”,没有有用的C ++子集是“安全的”

当然,对C ++的安全子集(严格意义上的“安全”,如LISP,Haskell,Java ......)的追求是注定无穷无尽的,作为安全的子集C ++很小,几乎没用,因为不安全的原语是规则而不是例外。 C ++中严格的内存安全性意味着没有指针,只引用自动存储类。但是在一个语言中程序员被定义信任,有些人会坚持使用一些(原则上)防止白痴的“智能指针”,即使在原始指针没有其他优势的情况下< em>一种特定的方式来避免程序状态。