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

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

循环引用:将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_; }; 

这里,父节点已被弱指针所取代。 在它所指的节点的生命周期中,它不再有发言权。 因此,如果最顶层的节点像前面的例子那样超出范围,那么虽然它对子节点拥有很强的引用,但是它的子节点并不强烈地引用父节点。 因此,没有强烈的对象的参考,它自己清理。 反过来,这会导致孩子失去一个强大的参照,导致他们清理,等等。 总之,这不会泄漏。 而只是战略上用weak_ptr <>replaceshared_ptr <>。

注意:以上同样适用于std :: shared_ptr <>和std :: weak_ptr <>,因为它可以提升:: shared_ptr <>和boost :: weak_ptr <>。

创build多个不相关的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; } 

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

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

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

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

 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() – 它不起作用。 相反,创build一个静态方法来创build类,并让它返回一个shared_ptr。

我没有任何麻烦地传递给shared_ptrs的引用。 只要确保它在被保存之前被复制(即没有引用作为类成员)。

这里有两件事要避免:

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

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

我们debugging几个星期的奇怪行为。

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

不仅仅是一个脚枪,而且肯定是一个挫败的源泉,直到你用C ++ 0x的方式来包装你的脑袋:你知道并且从<functional>爱的大多数谓词不能很好的与shared_ptr一起玩。 令人高兴的是, std::tr1::mem_fn与对象,指针和shared_ptr std::tr1::mem_fn工作,取代了std::mem_fun ,但是如果你想使用std::negatestd::not1std::plus或者其中任何一个老朋友与shared_ptr ,准备得到舒适与std::tr1::bind和可能的参数占位符以及。 在实践中,这实际上是更通用的,因为现在你基本上最终会为每个函数对象适配器使用bind ,但是如果你已经熟悉STL的便利函数,那么需要一些习惯。

这个DDJ文章涉及到这个主题,有很多示例代码。 几年前,当我首先想到如何去做的时候,我也会写博客 。

如果在堆上有很多小对象,但是它们并不真正“共享”,那么对于真正的小对象(比如char short )使用shared_ptr可能会造成开销。 boost::shared_ptr为g ++ 4.4.3和VS2008的Boost 1.42创build的每个新引用计数分配16个字节。 std::tr1::shared_ptr分配20个字节。 现在,如果你有一百万个不同的shared_ptr<char> ,那么就意味着你的内存的2000万字节在count = 1的时候没了。 更不用说间接成本和内存碎片了。 在您最喜欢的平台上尝试以下内容。

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

在类定义中给出一个shared_ptr <T>也是危险的。 改用enabled_shared_from_this。

在这里看到下面的post

在multithreading代码中使用shared_ptr时,需要小心。 当不同线程使用指向相同内存的shared_ptr对时,情况变得相对容易。

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

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

不太好理解的是未发布引用的问题。 如果一个对象被共享给多个shared_ptrs,那么它将不会被销毁,直到每一个对象被清零或者超出范围。 忽略这些参考文献是非常容易的,并且最终会隐藏在你以为已经完成的记忆中隐藏的东西。

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

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

我怀疑你的项目是这种做法可以让你陷入麻烦的一个很好的例子。

如果你有一个内存密集的应用程序,你真的需要单一的所有权,所以你的devise可以显式控制对象的生命周期

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

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

如果您有共享对象的registry(例如,所有活动实例的列表),则对象将永远不会被释放。 解决scheme:如循环依赖结构(见Kaz Dragon的答案),根据需要使用weak_ptr。

智能指针并不是一切,原始指针不能被消除

可能最危险的是,因为shared_ptr是一个有用的工具,人们将开始把它放在每一个地方。 由于普通的指针可能会被滥用,同样的人会search原始的指针,并尝试用string,容器或智能指针replace它们,即使它没有任何意义。 原始指针的正确使用将成为可疑的。 会有警察的指针。

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

当然,一个智能指针需要被转换为一个原始指针才能被用来反驳智能指针崇拜的这个说法,但是原始指针访问在operator*operator-> (或在get()显式显示,但在隐式转换中不隐含)足以给人一种印象,即这不是一个真正的转换,并且由这种非转换产生的原始指针是无害的临时。

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

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