我应该通过引用传递shared_ptr吗?

142

如何最好地传递shared_ptr?

目前我是这样传递shared_ptr函数参数的:

void function1( shared_ptr<TYPE>& value );

3
请查看我的回答shared_ptr by reference or by value?,其中我引用了Scott Meyers、Herb Sutter和Andrei Alexandrescu给出的明确答案。 - mloskot
3个回答

163
在受控情况下,您可以通过常量引用传递共享指针。不过请确保没有人同时删除该对象,如果您在给予引用时小心谨慎,这应该不难做到。
通常情况下,应当直接复制共享指针。这样可以给它带来预期的语义:每个包含共享指针副本的作用域都因为其"共享"拥有权而使对象保持活动状态。
唯一不总是按值传递的原因在于,在更新原子引用计数时,复制共享指针会带来一定的代价;然而,这可能并不是一个主要问题。
可选的离题: 既然主要问题已经解决,也许考虑一下您绝不应该使用共享指针的几种方式是有益的。以下是一个小思想实验。我们定义一个共享指针类型SF = std::shared_ptr<Foo>。为了考虑引用,而不是传递函数参数,我们看一下类型RSF = std::reference_wrapper<T>。也就是说,如果我们有一个共享指针SF p(std::make_shared<Foo>()),那么我们可以通过RSF w = std::ref(p)创建一个具有值语义的引用包装器。至此,设置完成。现在,每个人都知道指针容器是个雷区。因此,std::vector<Foo*> 将是一个维护困难的噩梦,并且由于未正确管理生命周期而产生任意数量的错误。更糟糕的是,从概念上讲,从容器存储指针的对象中清楚地知道谁拥有这些对象是不可能的。这些指针甚至可以是动态对象、自动对象和垃圾的混合物,没有人能够确定。因此,标准解决方案是使用 std::vector<SF>。这是正确使用共享指针的方法。另一方面,绝不能使用的是 std::vector<RSF>,这是一个无法管理的怪兽,实际上非常类似于原始裸指针的向量!例如,你不清楚你所持有的引用指向的对象是否还活着。获取共享指针的引用已经打败了它的整个目的。
第二个例子,假设我们像以前一样有一个共享指针 SF p。现在我们有一个函数 int foo(SF),我们希望同时运行它。通常的 std::thread(foo, p) 就可以正常工作,因为线程构造函数将其参数的副本。然而,如果我们说 std::thread(foo, std::ref(p)),我们会遇到各种麻烦:调用范围内的共享指针可能会过期并销毁对象,你将留下一个悬空引用和无效指针!
我希望这两个虽然有点牵强的例子能够让你更好地理解,什么时候真正需要通过copy传递共享指针。在设计良好的程序中,应该始终清楚谁负责哪些资源,并且当正确使用时,共享指针是一种非常好的工具。

1
@wolfgang:我刚刚发布了一个小的离题讨论,可能会解释清楚这个问题——但是请想象一下,如果几个并发上下文都引用同一个SP,那么一个上下文可能会将其重置(reset()),而另一个上下文不仅会得到对SP的修改引用,还可能得到指向资源的无效指针。 - Kerrek SB
3
稍微少用一点魔法:如果你不相信被调用者不会保存指向你的指针的引用,那么始终传递一个值,因为这表明,“你来负责”。通过const引用传递就像是“请帮我拿一下这个”,如果你认为他们可能会掉落它,那就别那样做。 - Kerrek SB
我的头都快晕了!在多线程代码中,通过值传递shared_ptr是首选,因为它具有原子引用增量/减量。但在单线程代码中,如果被调用者不应共享所有权,则传递值会发送错误的消息。在单线程代码中通过const引用传递可以避免增量/减量成本,但在多线程代码中,存在共享指针重置可能随时使我们的const引用无效的风险。那么,传递一个const指针是否更好,这样我们就可以使用if(ptr)进行测试?我想我们也必须使其每次使用都是原子的? - pbyhistorian
1
通常,你应该对问题和设计有一个清晰的心理模型,这样就不会感到害怕。首先,你实际上需要共享指针(而不是唯一指针)的情况非常少。一旦你确实需要共享所有权,你要么为你需要的服务付出代价,要么在你已经拥有共享并且可以保证生命周期时本地使用直接引用。如果你处于多线程环境中,你仍然需要考虑同步访问所管理的值本身。 - Kerrek SB
1
性能并不是通过引用传递共享指针的唯一原因。如果您通过值传递共享指针,则每当您在调试器中步入函数时,您都会首先进入共享指针的复制构造函数。这很烦人。为避免这种情况,我倾向于在可能的情况下通过引用传递共享指针。 - Kees-Jan
显示剩余4条评论

47

这取决于您的需求。被调用者是否需要共享对象的所有权?那么它需要自己的 shared_ptr 的副本。因此,通过值传递。

如果函数只需要访问由调用者拥有的对象,则继续传递(const)引用,以避免复制 shared_ptr 的开销。

C++ 中最佳实践是始终为您的对象定义清晰的所有权语义。没有通用的“总是这样做”来替代实际的思考。

如果您总是按值传递共享指针,那么会变得很昂贵(因为它们比原始指针更昂贵)。如果您从不这样做,那么使用共享指针就没有意义。

当新函数或对象需要共享指针所指向的对象的所有权时,请复制共享指针。


如果你从来没有这样做过,那么一开始使用共享指针就没有意义。你可能会想到“永远不要复制”,但是实际上是指“按值传递”。有些人可能只需要存储shared_ptr并始终通过const引用传递它。通过引用传递更快,因为它不会增加/减少引用计数器。 - Arpegius
@lionbest:不,我考虑的是按值传递。如果你从来没有按值传递过,那么使用shared_ptr就没有意义,因为只有一个客户端将持有实际的shared_ptr,那么它就不再是shared的了。 - jalf
5
你可以复制 void store( shared_ptr<T> const& sharedRef ) { this->mySharedRef=shredRef;}。如果传递的是按值传递,则计数器会增加两次。当然,你可以使用void store( shared_ptr<T> sharedRef ) { shredRef.swap(this->mySharedRef);} 来实现同样的功能。 - Arpegius

12

如果你使用引用传递,请通过const引用传递。这可以清楚地表明你是出于性能原因而进行引用传递。另外,尽可能使用make_shared,因为它可以节省一次间接引用,从而提高性能。


1
make_shared 总是一个好选择,但请注意它仅在创建第一个共享指针时才起作用。 - Kerrek SB
@Kerrek SB:并非总是如此。对于某些典型的实现,由于RTTI等原因,std::make_shared可能会使可执行映像文件的二进制大小有点膨胀,无论分配是否实际上有助于运行时性能。这对于桌面/服务器平台不应该是问题,但对于资源极其有限的嵌入式环境可能会成为问题。 - FrankHB
@FrankHB:那种“RTTI东西”为什么其他实现中也不存在呢?你每使用一个新类型就会增加二进制大小,所以最好尽量避免在同一类型中使用两种不同的实现。 - Kerrek SB
@KerrekSB:二进制文件大小会膨胀,因为该库在内部实现中使用了typeid。然而,我不能直接使用它,因为该类型未公开。例如,当启用RTTI时,libstdc++使用typeid(_Sp_make_shared_tag)获取由allocate_shared/make_shared分配的shared_ptr的删除器。如果我仍然需要其他地方使用RTTI,则无法禁用它,而不修改头文件源代码。实际上,结果的type_info只是一个占位符,真正需要运行时类型信息。因此,像boost::typeindex::type_id这样的东西应该能够改善情况。 - FrankHB

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