C ++ – 将引用传递给std :: shared_ptr或boost :: shared_ptr

如果我有一个需要使用shared_ptr的函数,将它的引用传递给它是不是更有效率(所以为了避免复制shared_ptr对象)? 什么是可能的不良副作用? 我设想了两种可能的情况:

1)里面的函数副本是由参数,如在

 ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp) { ... m_sp_member=sp; //This will copy the object, incrementing refcount ... } 

2)里面的函数只是使用参数,就像

 Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here { ... sp->do_something(); ... } 

在这两种情况下,我都看不到通过值而不是引用传递boost::shared_ptr<foo>的好理由。 按值传递只会“暂时”增加由于复制引起的引用计数,然后在退出函数范围时将其递减。 我可以俯视吗?

为了澄清,在阅读了几个答案之后,我完全赞同过早优化的问题,而且我总是试图首先在那个热点上工作。 如果你明白我的意思,我的问题更多的来自纯技术的代码观点。

一个独特的shared_ptr实例的目的是保证(尽可能)只要这个shared_ptr在范围内,它指向的对象将仍然存在,因为它的引用计数至less为1。

 Class::only_work_with_sp(boost::shared_ptr<foo> sp) { // sp points to an object that cannot be destroyed during this function } 

因此,通过使用对shared_ptr的引用,可以禁用该保证。 所以在你的第二种情况下:

 Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here { ... sp->do_something(); ... } 

你怎么知道sp->do_something()不会由于空指针而炸掉?

这完全取决于代码的那些“…”部分。 如果你在第一个“…”中调用了一些清除shared_ptr到同一个对象的副作用(在代码的另一部分的某处)呢? 而如果碰巧是该对象唯一的不同的shared_ptr呢? 再见对象,就在你要尝试和使用它的地方。

所以有两种方法来回答这个问题:

  1. 仔细检查整个程序的来源,直到确定对象在函数体中不会死亡。

  2. 将参数更改为独立的对象而不是引用。

此处适用的一般性build议:不要为了性能而对代码进行有风险的更改,直到在事件探查器中将您的产品定位在实际的情况下,并确定地测量出您想要做出的更改性能有显着差异。

更新评论者JQ

这是一个人为的例子。 这是故意的简单,所以这个错误是显而易见的。 在实际的例子中,这个错误并不是那么明显,因为它隐藏在真实的细节层面。

我们有一个函数可以在某个地方发送消息。 这可能是一个很大的消息,而不是使用一个std::string ,它可能被复制,因为它传递到多个地方,我们使用一个shared_ptrstring:

 void send_message(std::shared_ptr<std::string> msg) { std::cout << (*msg.get()) << std::endl; } 

(我们只是“发送”到这个例子的控制台)。

现在我们想添加一个设施来记住以前的消息。 我们需要下面的行为:一个variables必须包含最近发送的消息,但是当一个消息正在被发送的时候,这个消息必须没有以前的消息(variables应该在发送之前被重置)。 所以我们声明新的variables:

 std::shared_ptr<std::string> previous_message; 

然后根据我们指定的规则修改我们的function:

 void send_message(std::shared_ptr<std::string> msg) { previous_message = 0; std::cout << *msg << std::endl; previous_message = msg; } 

所以,在我们开始发送之前,我们放弃当前的消息,然后发送完成后,我们可以存储新的以前的消息。 都好。 以下是一些testing代码:

 send_message(std::shared_ptr<std::string>(new std::string("Hi"))); send_message(previous_message); 

正如所料,这打印Hi! 两次。

现在,来看看代码的Mr Maintainer,他认为:嗨, send_message参数是一个shared_ptr

 void send_message(std::shared_ptr<std::string> msg) 

显然可以改成:

 void send_message(const std::shared_ptr<std::string> &msg) 

想想这会带来的性能增强! (不要介意,我们即将通过某个频道发送一个通常较大的消息,因此性能增强将会如此之小以至于无法衡量)。

但真正的问题是,现在testing代码将显示未定义的行为(在Visual C ++ 2010debugging版本,它崩溃)。

Maintainer先生对此感到惊讶,但为send_message增加了一个防御性检查,试图阻止这个问题的发生:

 void send_message(const std::shared_ptr<std::string> &msg) { if (msg == 0) return; 

但是,当然,它仍然继续,并崩溃,因为调用send_messagemsg不会为空。

正如我所说,所有的代码在一个简单的例子中如此接近,很容易find这个错误。 但是在真正的程序中,相互之间有指针的可变对象之间有更复杂的关系,很容易出错,很难构造必要的testing用例来检测错误。

简单的解决scheme是让函数能够依赖于shared_ptr继续为非null的简单解决scheme是为函数分配自己的真实shared_ptr ,而不是依赖于对现有shared_ptr的引用。

缺点是复制一个shared_ptr不是免费的:即使是“无锁”的实现也必须使用一个互锁操作来保证线程的安全。 因此,可能会出现这样的情况,即通过将shared_ptr更改为shared_ptr &来显着提高程序的速度。 但是,这并不是一个可以安全地对所有程序做出的改变。 它改变了程序的逻辑意义。

请注意,如果我们在整个过程中使用std::string而不是std::shared_ptr<std::string> ,则会发生类似的错误,而不是:

 previous_message = 0; 

为了清除这个信息,我们说:

 previous_message.clear(); 

那么症状将是意外发送一个空的消息,而不是未定义的行为。 一个非常大的string的额外副本的成本可能比复制一个shared_ptr的成本要重要得多,所以权衡可能是不同的。

我发现自己不同意最高票的答案,所以我去寻找专家的意见,他们在这里。 从http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything

香草萨特:“当你通过shared_ptr的,副本是昂贵的”

Scott Meyers:“关于shared_ptr没什么特别的地方,不pipe你是按值传递,还是通过引用传递shared_ptr,都可以使用与其他用户定义的types完全相同的分析,人们似乎认为shared_ptr能够解决某些问题所有的pipe理问题,因为它很小,通过价值是必要的廉价,它必须被复制,并有一个相关的成本…它通过价值是昂贵的,所以如果我可以摆脱它与我的程序中正确的语义,我要通过引用传递给const或引用而不是“

香草萨特:“总是通过引用const来传递它们,偶尔也许是因为你知道你所称的可能会修改你从中得到的参考,也许那么你可能会传递值…如果你把它们作为参数复制哦我的天哪,你几乎从来都不需要因为这个引用计数而受到影响,而且你应该通过引用来传递它,所以请做那个“

更新:Herb在这里扩展了这里: http : //herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ ,虽然故事的寓意是您不应该通过shared_ptr的“,除非你想使用或操纵智能指针本身,如分享或转让所有权。

我会build议反对这种做法,除非你和你一起工作的其他程序员真的知道你在做什么。

首先,你不知道你的类的接口如何发展,你想阻止其他程序员做坏事。 通过引用传递shared_ptr不是程序员期望看到的东西,因为它不是惯用的,而且很容易错误地使用它。 防守编程:使界面难以正确使用。 通过引用传递将在稍后引发问题。

其次,不要优化,直到你知道这个class级将成为一个问题。 首先是configuration文件,然后如果你的程序真的需要通过引用传递给予的提升,那么也许。 否则,不要为了小小的东西而花费大量的时间(即额外的N个指令来传递值),而是担心devise,数据结构,algorithm和长期的可维护性。

是的,参考是好的。 你不打算给方法共享所有权; 它只是想与它合作。 您也可以参考第一个案例,因为您无论如何都要复制它。 但是对于第一种情况,它需要所有权。 有这个伎俩仍然只复制一次:

 void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) { m_sp_member.swap(sp); } 

你也应该复制,当你返回它(即不返回一个参考)。 因为你的类不知道客户端在做什么(它可以存储一个指针,然后发生大爆炸)。 如果后来certificate这是一个瓶颈(第一个configuration文件!),那么你仍然可以返回一个参考。


编辑 :当然,正如其他人指出的,这只有当你知道你的代码,并知道你没有以某种方式重置传递的共享指针是真实的。 如果有疑问,只要通过价值。

通过const&传递shared_ptr是明智的。 它不会引起麻烦(除非在函数调用期间被引用的shared_ptr被删除,如Earwicker详细说明的那样),如果你传递了很多这些信息,它可能会更快。 记得; 默认的boost::shared_ptr是线程安全的,所以复制它包含一个线程安全增量。

尝试使用const&而不只是& ,因为临时对象可能不会被非const引用传递。 (尽pipeMSVC中的语言扩展允许你这样做)

在第二种情况下,这样做更简单:

 Class::only_work_with_sp(foo &sp) { ... sp.do_something(); ... } 

你可以称之为

 only_work_with_sp(*sp); 

我会避免一个“普通”的引用,除非该函数明确地可以修改指针。

在调用小函数时, const &可能是一个明智的微优化 – 例如,为了进一步优化,比如内联一些条件。 此外,增量/减量 – 因为它是线程安全的 – 是一个同步点。 不过,我不希望这会在大多数情况下产生很大的影响。

一般来说,你应该使用更简单的风格,除非你有理由不这样做。 然后,不pipe是使用const &还是一致的,或者添加一个注释,为什么只在几个地方使用它。

我会主张通过const引用传递共享指针 – 一个语义,指针传递的函数不拥有指针,这对开发人员来说是一个干净的习惯用法。

唯一的缺陷是在多个线程程序中,由共享指针指向的对象在另一个线程中被销毁。 所以在单线程程序中使用共享指针的const引用是安全的。

通过非const引用传递共享指针有时是危险的 – 原因是函数可能调用的交换函数和重置函数,以便在函数返回之后销毁仍被视为有效的对象。

我想 – 这不是过早的优化,而是要避免不必要的CPU周期浪费,当你清楚你想要做什么,编码习惯已被你的开发人员所采用。

只是我2美分:-)

似乎这里的所有优点和缺点实际上可以推广到任何通过引用传递的types,而不仅仅是shared_ptr。 在我看来,你应该知道通过引用,const引用和值传递的语义,并正确使用它。 但是通过引用传递shared_ptr绝对没有什么错,除非你认为所有的引用都是不好的…

回到这个例子:

 Class::only_work_with_sp( foo &sp ) //Again, no copy here { ... sp.do_something(); ... } 

你怎么知道sp.do_something()不会因为悬挂指针而炸掉?

事实是,shared_ptr与否,const与否,这可能发生,如果你有一个devise缺陷,如直接或间接共享sp之间的所有权,错过一个对象, delete this ,你有一个循环所有权或其他所有权错误。

有一件我还没有提到的事情是,当通过引用传递共享指针时,如果要通过对基类共享指针的引用传递派生类共享指针,将会失去隐式转换。

例如,这段代码会产生一个错误,但是如果你改变了test() ,那么共享指针不会被引用传递。

 #include <boost/shared_ptr.hpp> class Base { }; class Derived: public Base { }; // ONLY instances of Base can be passed by reference. If you have a shared_ptr // to a derived type, you have to cast it manually. If you remove the reference // and pass the shared_ptr by value, then the cast is implicit so you don't have // to worry about it. void test(boost::shared_ptr<Base>& b) { return; } int main(void) { boost::shared_ptr<Derived> d(new Derived); test(d); // If you want the above call to work with references, you will have to manually cast // pointers like this, EVERY time you call the function. Since you are creating a new // shared pointer, you lose the benefit of passing by reference. boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d); test(b); return 0; } 

我假定你熟悉过早的优化 ,并且要求这是为了学术目的,或者是因为你已经隔离了一些预先存在的性能不佳的代码。

通过引用传递没关系

通过const引用传递更好,通常可以使用,因为它不会强制指向对象的常量。

由于使用引用,您不会有丢失指针的风险。 该引用是您在堆栈中早些时候拥有智能指针的副本的证据,并且只有一个线程拥有一个调用堆栈,因此预先存在的副本不会消失。

由于您提到的原因,使用参考通常更高效, 但不能保证 。 请记住,解除引用对象也可以起作用。 如果您的编码风格涉及许多小函数,那么指针将从函数传递到函数以在使用之前起作用,那么您理想的参考用法场景就是这样。

您应该始终避免将智能指针存储为参考。 您的Class::take_copy_of_sp(&sp)示例显示正确的用法。

假设我们不关心const的正确性(或者更多,你的意思是允许函数能够修改或者共享被传入的数据的所有权),通过值传递boost :: shared_ptr比通过引用传递它更安全我们允许原始的boost :: shared_ptr来控制它自己的生命周期。 考虑以下代码的结果…

 void FooTakesReference( boost::shared_ptr< int > & ptr ) { ptr.reset(); // We reset, and so does sharedA, memory is deleted. } void FooTakesValue( boost::shared_ptr< int > ptr ) { ptr.reset(); // Our temporary is reset, however sharedB hasn't. } void main() { boost::shared_ptr< int > sharedA( new int( 13 ) ); boost::shared_ptr< int > sharedB( new int( 14 ) ); FooTakesReference( sharedA ); FooTakesValue( sharedB ); } 

从上面的例子中,我们看到,通过引用传递sharedA允许FooTakesReference重置原始指针,从而将其使用次数减less到0,销毁它的数据。 但是, FooTakesValue不能重置原始指针,保证sharedB的数据仍然可用。 当另一个开发者不可避免地出现并试图搭载共享的脆弱的存在时,就会出现混乱。 然而,幸运的共享B开发者却早早地回家,因为他的世界都是对的。

在这种情况下,代码的安全性远远超过了复制速度的提高。 同时,boost :: shared_ptr是为了提高代码的安全性。 如果某些事情需要这种利基优化,从副本转到参考将会容易得多。

桑迪写道:“似乎所有的优点和缺点实际上可以推广到任何通过引用传递的types,而不仅仅是shared_ptr。

在某种程度上是正确的,但是使用shared_ptr的意义在于消除关于对象生命周期的问题,并让编译器为你处理。 如果您要通过引用传递共享指针,并允许引用计数对象的非客户端调用可能释放对象数据的非常量方法,那么使用共享指针几乎是毫无意义的。

我在前面的句子中写了“几乎”,因为性能可能是一个问题,在极less数情况下可能是合理的,但我也会自己避免这种情况,并自己寻找所有可能的其他优化解决scheme,比如认真看待在增加另一层次的间接,懒惰的评价等。

在作者身上存在的代码,甚至是作者的记忆,需要对行为进行隐含的假设,特别是关于对象生命周期的行为,需要清晰,简洁,可读的文档,然后许多客户端将不会阅读它! 简单几乎总是胜过效率,几乎总是有其他方法是有效的。 如果您确实需要通过引用传递值以避免通过引用计数对象(和等号运算符)的复制构造函数进行深度复制,那么也许您应该考虑如何将深度复制数据作为引用计数指针快速复制。 (当然,这只是一种devisescheme,可能不适用于您的情况)。

我曾经在一个项目中工作,这个项目的原理是非常有价值的通过智能指针。 当我被要求做一些性能分析时 – 我发现,对于智能指针的引用计数器的增量和减量,应用程序花费在利用的处理器时间的4-6%之间。

如果您想按照价值传递智能指针,以避免在Daniel Earwicker描述的奇怪情况下出现问题,请确保您了解您为此付出的代价。

如果你决定使用一个引用,那么使用const引用的主要原因是当你需要将inheritance你在接口中使用的类的类的共享指针传递给对象时,可以隐式上传。

除了litb所说的之外,我想指出的是,可能在第二个例子中通过const引用来传递,这样你肯定不会意外地修改它。

 struct A { shared_ptr<Message> msg; shared_ptr<Message> * ptr_msg; } 
  1. 按价值传递:

     void set(shared_ptr<Message> msg) { this->msg = msg; /// create a new shared_ptr, reference count will be added; } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced; 
  2. 通过参考传递:

     void set(shared_ptr<Message>& msg) { this->msg = msg; /// reference count will be added, because reference is just an alias. } 
  3. 通过指针传递:

     void set(shared_ptr<Message>* msg) { this->ptr_msg = msg; /// reference count will not be added; } 

每一个代码段必须有一定的意义。 如果您在应用程序中的任何位置传递一个共享指针,这意味着“我不确定其他地方发生了什么,因此我赞成原始安全性 ”。 这不是我所说的一个很好的信任签名给其他可以查阅代码的程序员。

无论如何,即使一个函数获得了一个const引用,并且你是“不确定”的,你仍然可以在函数头部创build一个共享指针的副本,为指针添加一个强引用。 这也可以被看作是关于devise的暗示(“指针可以在别处修改”)。

所以是的,国际海事组织,默认应该是“ 通过const参考 ”。