当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《C++-将引用传递给std::shared_ptr或boost::shared_ptr》经验,为你挑选了6个好方法。

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

1)在函数内部,复制由参数组成,如

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

2)在函数内部仅使用参数,例如

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

我无法在两种情况下都看到传递boost::shared_ptrby值而不是引用的充分理由.按值传递只会"暂时"增加由于复制而引用的引用计数,然后在退出函数范围时减少它.我忽略了什么吗?

只是为了澄清,在阅读了几个答案之后:我完全赞同过早优化的问题,而且我总是试着先在热点上进行首次剖析.如果你知道我的意思,我的问题更多来自纯粹的技术代码观点.



1> Jon..:

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

Herb Sutter:"当你通过shared_ptr时,副本很贵"

Scott Meyers:"当涉及到你是通过值传递它还是通过引用传递它时,shared_ptr没什么特别的.使用与你用于任何其他用户定义类型的完全相同的分析.人们似乎有这种看法,shared_ptr以某种方式解决所有的管理问题,因为它很小,所以通过价值传递必然是便宜的.它必须被复制,并且有相关的成本......按价值传递它是昂贵的,所以如果我可以逃脱它在我的程序中有适当的语义,我将通过引用传递给const或引用"

Herb Sutter:"总是通过参考const来传递它们,偶尔也许是因为你知道你所谓的可能会修改你从中得到引用的东西,也许你可能会通过值传递...如果你把它们复制为参数哦我的天哪,你几乎从不需要碰到那个引用计数,因为它无论如何都被保留了,你应该通过引用传递它,所以请这样做"

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


Herb Sutter:"偶尔因为你知道你所谓的内容可能会修改你所引用的东西".对于一个小案子,这个小豁免,所以它与我的答案并不矛盾.问题依然存在:你怎么知道*使用const ref是安全的?很容易在一个简单的程序中证明,在复杂的程序中不那么容易.但是,嘿,这个*是*C++,因此我们赞成对几乎所有其他工程问题进行过早的微优化,对吧?!:)
很好找!很高兴看到有关该主题的两位最重要的专家公开反驳SO的传统智慧.
"无论是通过值传递还是通过引用传递它,shared_ptr都没有什么特别之处" - 我真的不同意这一点.这很特别.就个人而言,我宁愿把它安全地玩,并且会受到轻微的性能影响.如果有一个特定的代码区域我需要优化然后肯定,我会看看const ref的shared_ptr传递的性能优势.
值得注意的是,虽然已经就过度使用`shared_ptr'达成了一致意见,但没有就传递价值与参考问题达成一致意见.
斯科特迈尔斯:"所以,如果我能在程序中使用适当的语义来解决它..."即完全没有与我的答案相矛盾,这指出确定将参数更改为"const&"是否会影响语义是只在非常简单的程序中轻松.
由于最近这次被严重烧伤,我不得不同意这个答案.你可以结束一系列相互调用的函数,每个函数都引用一个共享指针来传递许多函数.如果最里面的函数操纵集合,则在链的顶部找到共享指针,*boom*,整个链都会崩溃.这些东西可以感染代码库,并且随着它们的增长和变化而变得非常难以验证.这是一个不成熟的微优化.
@David:您是否同意您的问题不是特定于shared_ptr?你应该通过一长串函数传递一个字符串(或任何其他对象),因为最里面的一个可以操纵它来自的集合吗?
@Jon:它并不完全特定于`shared_ptr`.它特定于与`shared_ptr`的典型用例有重大重叠的特定用例.一个特定于`shared_ptr`(或其他智能指针)的部分是,程序员并不总是认为他们调用方法的对象就像对这些方法的参数一样.所以这可能会让你陷入困境.
-1尽管这些"专家"提出了这样的说法,但他们显然错误,正如Daniel Earwicker已经解释的那样.写作"我发现自己不同意最高投票的答案",当最高投票答案显然是可证实的事实陈述是荒谬的.
总的来说,这是一个糟糕的建议.在这个SO问题的背景下,你应该坚持最佳实践,即......不要预先优化.只需传递std :: shared_ptr并递增计数.当且仅当您发现自己处于性能受到shared_ptr损害的情况时,而不是常见情况,那么您可以讨论一些高级的特殊情况编程技巧.
@JoeGauterin我同意代码是关于通信的,并且考虑与将指针或数据交给另一个函数或类相关联的生命周期/合同是很重要的.我的牛肉是关心shared_ptr的成本.正是这种对话就是为什么C++会继续缓慢下降到默默无闻的原因.关于做x,y或z的绝对"最快"方式,它由细枝末节主导.如果以这种特殊方式对其进行编码,会导致编译器发出更好的汇编.答案应该是以合同为重点,而不是以绩效为重点.

2> Daniel Earwi..:

不同shared_ptr实例的要点是保证(尽可能)只要shared_ptr它在范围内,它指向的对象仍然存在,因为它的引用计数至少为1.

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

因此,通过使用对a的引用shared_ptr,您将禁用该保证.所以在你的第二个案例中:

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

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

这一切都取决于代码的那些'......'部分.如果你在第一个'...'中调用某些东西会产生副作用(在代码的另一部分的某个地方)清除shared_ptr同一个对象会怎么样?如果它恰好是该shared_ptr对象唯一的不同之处呢?再见对象,就在你要尝试使用它的地方.

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

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

    将参数更改回不同的对象而不是引用.

适用于此处的一般建议:为了提高性能,不要费心对代码进行有风险的更改,直到您在剖析器中将实际情况计时产品并最终确定您要进行的更改将使性能差异显着.

评论者JQ的更新

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

我们有一个函数可以在某处发送消息.它可能是一个很大的消息,所以std::string我们使用a shared_ptr到字符串,而不是使用可能被复制的消息,因为它被传递到多个地方:

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

(我们只是将它"发送"到这个例子的控制台).

现在我们要添加一个工具来记住上一条消息.我们需要以下行为:必须存在包含最近发送的消息的变量,但是当前正在发送消息时,必须没有先前的消息(该变量应该在发送之前重置).所以我们声明了新的变量:

std::shared_ptr previous_message;

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

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

因此,在我们开始发送之前,我们丢弃当前的上一条消息,然后在发送完成后,我们可以存储新的上一条消息.都好.这是一些测试代码:

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

正如预期的那样,这会打印Hi!两次.

现在来看看Maintainer先生,他看着代码并想:嘿,那个参数send_messageshared_ptr:

void send_message(std::shared_ptr msg)

显然可以改为:

void send_message(const std::shared_ptr &msg)

想想这将带来的性能提升!(没关系,我们将要通过某个频道发送一个典型的大型消息,因此性能提升将非常小,以至于无法测量).

但真正的问题是,现在测试代码将呈现未定义的行为(在Visual C++ 2010调试版本中,它会崩溃).

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

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

但当然它仍然会继续崩溃,因为msgsend_message调用时它永远不会为null .

正如我所说,所有代码在一个简单的例子中如此接近,很容易找到错误.但在实际方案,持有指向对方可变对象之间的关系更复杂,很容易做出错误,并努力构建必要的测试用例来检测错误.

简单的解决方案是,您希望函数能够依赖于shared_ptr始终为非null的函数,该函数可以为函数分配自己的true shared_ptr,而不是依赖于对现有函数的引用shared_ptr.

缺点是复制的a shared_ptr不是免费的:即使是"无锁"的实现也必须使用互锁操作来保证线程保证.因此,有些情况下可以通过将a更改shared_ptr为a 来显着加快程序的速度shared_ptr &.但这不是一个可以安全地对所有程序进行的改变.它改变了程序的逻辑含义.

请注意,如果我们使用的是std::string整个而不是代码,则会发生类似的错误std::shared_ptr:

previous_message = 0;

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

previous_message.clear();

然后症状是意外发送空消息,而不是未定义的行为.一个非常大的字符串的额外副本的成本可能比复制a的成本更重要shared_ptr,因此权衡可能是不同的.


传入的shared_ptr已经存在于调用站点的作用域中.您可能能够创建一个精心设计的场景,其中此问题中的代码会因悬空指针而爆炸,但我认为您的问题比参考参数更大!
它可以存储在成员中.你可以打电话来清除那个成员.smart_ptr的重点是避免在嵌套在调用堆栈周围的层次结构或范围中协调生命周期,这就是为什么最好假设生命周期在这样的程序中不这样做.
@DanielEarwicker完全同意你的所有观点,并对反对派的水平感到惊讶.让你的担忧更加相关的东西是线程,当涉及到对象保证时,对象的有效性变得更加重要.好答案.
虽然这不是我的观点!如果你认为我所说的是与我的代码有关的东西,你可能没有理解我.我所说的首先是shared_ptr存在的一个不可避免的含义:许多对象生命周期并不仅仅与函数调用有关.
不久前,我追查了一个非常严重的错误,该错误是由于传递了对共享指针的引用.代码处理对象的状态更改,当它注意到对象的状态已更改时,它将其从先前状态中的对象集合中移除,并将其移动到处于新状态的对象集合中.remove操作销毁了对象的最后一个共享指针.已在对集合中的共享指针的引用上调用成员函数.繁荣.Daniel Earwicker是对的.
但是,`shared_ptr v`的目的是作为更好的`T*v`,它的具体优势是什么?它会阻止物体过早被摧毁.再次将其更改为`shared_ptr &v`,您就放弃了这一优势.`shared_ptr`的重点是复制它足够便宜,你可以广泛地依赖它,这样你就不必负责分析对象生命周期的各个方面.这适用于您*需要*按设计共享,以及您有复杂的重叠对象生命周期的情况,因此手动分析将非常困难.
第二个例子中的错误与`shared_ptr`无关.用其他任何东西替换`std :: shared_ptr `参数 - 例如`std :: string` - 代码仍然通过全局`previous_message'破坏`msg`参数来打破.唯一的区别是`std :: shared_ptr `版本会崩溃而不是输出错误,这是因为`send_message()`在使用它之前没有确保指针有效(它是无论是否通过引用传递`shared_ptr`,都需要做.
@JoshTownzen - 看到答案的最后一部分,开始"请注意,如果我们在整个...中使用std :: string会发生类似的错误",它说的完全相同.您可能已经错过了这一点,即程序最初可能绝对正确,但如果您将`shared_ptr`更改为`const shared_ptr&`则会变得绝对错误.
@Tolga - 因为它也适用于其他变量类型的其他情况,你得出结论它不适用于这种情况?*那*是有缺陷的逻辑.
@Cartroo - 我怀疑大多数人都喜欢Sutter的建议,但不是很认真.他的建议的简短版本是"首选按值,*或&传递对象,而不是通过智能指针传递对象."但他添加了警告"注意这假设指针没有别名.如果智能指针参数,你必须小心可能是别名,但在这方面它与任何其他别名对象没什么不同." 那个警告正是*我的答案!我只是详细了解混叠的含义以及导致问题的原因[续...]
[续...]但是一旦你得到了,那么问题就是你是否想要冒险发现令人愉快的有趣和微妙的别名错误,换取"快速"代码,或者你是否更喜欢更强保证正确性,这样你就可以测量和投资优化工作(尽管要小心避免混淆错误),这是真正必要的.我强烈推荐后者.

3> Mark Kegel..:

除非你和你合作的其他程序员真的知道你们在做什么,否则我会反对这种做法.

首先,你不知道你的类的接口是如何演变的,你想要阻止其他程序员做坏事.通过引用传递shared_ptr并不是程序员应该看到的东西,因为它不是惯用的,这使得它很容易被错误地使用.防御性程序:使界面难以正确使用.通过引用传递将在以后引发问题.

其次,在你知道这个特定类会成为一个问题之前不要进行优化.首先描述,然后如果你的程序真的需要通过引用传递给出的提升,那么也许.否则,不要为小东西(即通过值传递额外的N指令)而烦恼,而是担心设计,数据结构,算法和长期可维护性.



4> Johannes Sch..:

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

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

您还应该在退货时复制(即不返回参考).因为你的类不知道客户端正在做什么(它可以存储指向它的指针,然后发生大爆炸).如果它后来证明它是一个瓶颈(第一个配置文件!),那么你仍然可以返回一个引用.


编辑:当然,正如其他人所指出的那样,只有你知道你的代码并且知道你没有以某种方式重置传递的共享指针,这才是真的.如果有疑问,只需按值传递.



5> Magnus Hoff..:

通过shared_ptrs 是明智的const&.它不会引起麻烦(除非在shared_ptr函数调用期间删除引用的不太可能的情况,如Earwicker所详述),如果你传递了很多这些内容,它可能会更快.记得; 默认boost::shared_ptr是线程安全的,因此复制它包括线程安全增量.

尝试使用const&而不仅仅是&因为临时对象可能不会被非const引用传递.(尽管MSVC中的语言扩展允许您无论如何都可以这样做)


是的,我总是使用const引用,我只是忘了把它放在我的例子中.无论如何,MSVC允许将非const引用绑定到temporaries而不是bug,但是因为默认情况下它将"C/C++ - > Language - > Disable Language Extension"属性设置为"NO".启用它,它将无法编译它们......

6> Greg Rogers..:

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

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

你可以称之为

only_work_with_sp(*sp);


如果您在不需要获取指针副本时采用约定来使用对象引用,那么它也可用于记录您的意图.它还为您提供了使用const引用的机会.
推荐阅读
LEEstarmmmmm
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有