Should I pass a shared_ptr by reference?
传递shared_ptr的最佳实践是什么?
目前,我像这样传递shared_ptr函数参数:
1
| void function1( shared_ptr<TYPE>& value ); |
-
通过引用还是按值查看我对shared_ptr的回答? 在这里,我引用了Scott Meyers,Herb Sutter和Andrei Alexandrescu给出的明确答案。
在受控情况下,可以通过常量引用传递共享指针。确保没有人同时删除该对象,尽管如果您要小心向谁提供引用,这应该不太困难。
通常,您应该将共享指针作为直接副本传递。这赋予了它预期的语义:每个包含共享指针副本的范围都通过对象在所有权中的"共享"使对象保持活动状态。
不总是按值传递的唯一原因是,由于原子引用计数的更新,复制共享指针要付出一定的代价。但是,这可能不是主要问题。
可选题外话:
既然已经回答了主要问题,那么考虑一些永远不应该使用共享指针的方法也许是有益的。这是一个小小的思想实验。让我们定义一个共享指针类型SF = std::shared_ptr。为了考虑引用,而不是传递函数参数,让我们看一下类型RSF = std::reference_wrapper< T >。也就是说,如果我们有一个共享的指针SF p(std::make_shared());,那么我们可以通过RSF w = std::ref(p);使用值语义创建一个引用包装。设置非常重要。
现在,每个人都知道指针容器是雷区。因此std::vector将是维护的噩梦,并且任何不正确的生命周期管理都会引起许多错误。从概念上讲,更糟的是,永远不清楚谁拥有容器存储其指针的对象。指针甚至可以是指向动态对象,自动对象和垃圾的指针的混合。没有人能告诉。因此,标准解决方案是改用std::vector。这是使用共享指针的正确方法。另一方面,您永远不能使用的是std::vector-这是一个难以处理的怪物,实际上与裸指针的原始向量非常相似!例如,尚不清楚您持有引用的对象是否仍然存在。引用共享指针已经破坏了它的全部目的。
对于第二个示例,假设我们像以前一样有一个共享指针SF p。现在,我们有一个要同时运行的函数int foo(SF)。通常的std::thread(foo, p)效果很好,因为线程构造函数会复制其参数。但是,如果我们说std::thread(foo, std::ref(p)),就会遇到各种各样的麻烦:调用范围中的共享指针可能到期并破坏该对象,并且您将剩下一个悬挂的引用和一个无效的指针!
我希望当您确实希望共享指针通过副本传递时,这两个公认的相当人为的示例能为您提供一些启示。在一个设计良好的程序中,应始终清楚由谁负责哪些资源,并且在正确使用时,共享指针是完成工作的好工具。
-
您能否详细说明"同时删除对象"的含义?您是在暗示通过参考传递任何东西时,除了"通常"的危险之外,还有其他危险吗?
-
@wolfgang:我刚刚发布了一个题外话,可能解释了这一点,但想像一下,多个并发上下文引用了同一个SP。那么一个可能会reset()它??,另一个可能不仅会以对SP的修改后的引用结尾,而且还会以指向资源的无效指针结尾。
-
是的,这就是我所说的"通常"危险。但是然后,如果并发线程在Im复制它时修改了SP,那么我也会遇到麻烦(尽管在某些std :: SP实现中我可能会避免这样做)。如果我传递一个std :: string对象(const std :: string&或按值),也是如此。
-
初学者(和其他非向导)摘要...如果您确切知道传递const std :: string&时会发生什么,请随时对std :: shared_ptr进行相同操作,以获取与无关紧要的性能提升取决于实际情况。如果const what&对您来说是黑魔法,请远离它,不值得冒险。
-
@wolfgang:少了一点魔术:如果您不相信被调用者不存储对指针的引用,那么请始终传递一个值,因为它表示"在这里,您要承担责任"。通过const-reference传递就像"请稍等一会儿",如果您认为他们可能会放弃它,那就不要。
-
我的头在旋转!因此,在多线程代码中,最好通过值传递shared_ptr,因为原子引用的增加/减少。但是在单线程代码中,被调用者不应该共享所有权,按值传递会发送错误消息。在单线程代码中通过const引用传递可避免增加/减少开销,但是在多线程代码中,shared_ptr重置有可能随时使我们的const引用无效。那么通过const ptr会更好,以便我们可以使用if(ptr)进行测试吗?我想我们也必须使它的每一个用途都原子化吗?
-
@pbyhistorian:通常,您应该对问题和设计有一个清晰的思维模型,然后就不那么令人恐惧了。首先,您实际上想要共享指针(而不是唯一指针)的时间将非常短。一旦确实确实需要共享所有权,您将为所需的服务付费,或者在已经拥有共享并可以保证寿命的情况下在本地使用直接引用。如果仍然处于多线程设置中,则仍然需要考虑同步对托管值本身的访问。
-
性能不是通过引用传递共享指针的唯一原因。如果按值传递共享指针,那么每当您进入调试器中的函数时,您都将首先进入共享指针的副本构造函数。真烦人为了避免这种情况,我倾向于尽可能地通过引用传递共享指针。
那取决于你想要什么。被调用者是否应该共享对象的所有权?然后,它需要自己的shared_ptr副本。因此,将其传递给价值。
如果函数仅需要访问调用者拥有的对象,则继续并传递(const)引用,以避免复制shared_ptr的开销。
C ++的最佳实践始终是为对象明确定义所有权语义。没有通用的"总是这样做"来代替实际思想。
如果您总是按值传递共享指针,那么它将变得很昂贵(因为复制它们比原始指针要昂贵得多)。如果您从不这样做,那么首先使用共享指针是没有意义的。
当新功能或对象需要共享指针的所有权时,请复制共享指针。
-
"如果您从不这样做,那么首先就没有必要使用共享指针。" -您想到了"从不复制",而是提到"按价值传递"。有些人可能只需要存储shared_ptr并始终通过const引用传递它。通过参考传递更快,因为它不增加/减少参考计数器。
-
@lionbest:不,我认为按价值传递。如果您从不按值传递值,则shared_ptr中就没有意义,因为只有一个客户端会持有一个实际的shared_ptr,然后才不共享它。
-
您可以复制void store( shared_ptr< T > const& sharedRef ) { this->mySharedRef=shredRef;}。如果shredRef将通过值传递,它将使计数器递增两次。当然,您可以执行void store( shared_ptr< T > sharedRef ) { shredRef.swap(this->mySharedRef);}来执行相同的操作。
如果通过引用传递,则通过const引用传递。这清楚地表明,出于性能原因,您正在通过ref。另外,请尽可能使用make_shared,因为它可以保存间接寻址,从而提高性能。
-
make_shared始终是一件好事,但是请注意,它仅在创建第一个共享指针时起作用。
-
@Kerrek SB:并非总是如此。对于某些典型的实现,std::make_shared会由于RTTI的原因而使可执行映像的二进制大小有些膨胀,无论分配实际上是否会有利于运行时性能。对于台式机/服务器平台,这应该不是问题,但是对于资源极其有限的嵌入式环境,则可能是问题。
-
@FrankHB:在其他实现中还不存在" RTTI内容"吗?您总是会为使用的每种新类型增加二进制文件的大小,因此充其量您可以尝试避免对同一类型使用两种不同的实现。
-
@KerrekSB:二进制大小会膨胀,因为库将typeid用于内部实现。但是,我不能直接使用它,因为该类型没有公开。例如,启用RTTI时,libstdc ++使用typeid(_Sp_make_shared_tag)获取由allocate_shared / make_shared分配的shared_ptr的删除程序。如果我仍然需要其他地方的RTTI,我没有选择禁用它而不修改标题中的源代码的选择。实际上,结果type_info是占位符,实际上不需要任何运行时类型信息。所以像boost::typeindex::type_id这样的东西应该会使情况变得更好。