C ++-传递对std :: shared_ptr或boost :: shared_ptr的引用


115

如果我有一个需要使用的函数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>按值传递而不是按引用传递。传递值只会由于复制而“临时”增加引用计数,然后在退出功能范围时减少引用计数。我在俯视什么吗?

为了澄清,在阅读了几个答案之后:我完全同意过早优化的问题,并且我总是尝试先介绍热点,然后再进行热点工作。如果您知道我的意思,我的问题更多是从纯粹的技术代码角度来看。


我不知道您是否可以修改问题的标签,但请尝试在其中添加增强标签。我尝试查找此问题,但找不到任何内容,因为我在寻找boost和smartpointer标记。所以我
Edison Gustavo Muenz 2009年

Answers:


113

不同shared_ptr实例的目的是(尽可能)保证,只要shared_ptr它在范围内,它指向的对象仍然存在,因为它的引用计数至少为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. 将参数改回为唯一的对象,而不是引用。

适用于此的一般建议:不要为了性能而对代码进行冒险的更改,直到您在探查器中将产品置于实际情况下计时并最终确定要进行的更改会导致性能上的显着差异。

更新评论者JQ

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

我们有一个可以在某处发送消息的功能。它可能是一条大消息,因此std::string,我们使用a shared_ptr来表示字符串,而不是使用可能会在传递到多个位置时被复制的a:

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

(对于本示例,我们只是将其“发送”到控制台)。

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

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

然后,我们根据指定的规则修改功能:

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

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

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

和预期的一样,此打印Hi!两次。

随之而来的是Maintainer先生,他查看代码并认为:嘿,那个参数send_messageshared_ptr

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

显然可以更改为:

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

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

但是真正的问题是,现在测试代码将表现出不确定的行为(在Visual C ++ 2010调试版本中,它会崩溃)。

维护者先生对此感到惊讶,但send_message为阻止问题的发生添加了防御性检查:

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

但是当然它仍然会继续前进并崩溃,因为调用msg时永远不会为null send_message

正如我所说,在一个简单的示例中,所有代码如此紧密地结合在一起,很容易发现错误。但在实际方案,持有指向对方可变对象之间的关系更复杂,很容易做出错误,并努力构建必要的测试用例来检测错误。

一个简单的解决方案shared_ptr是让函数分配自己的true shared_ptr,而不是依赖现有的引用,从而使函数能够始终依赖于非null shared_ptr

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

请注意,如果我们在全文中使用std::string而不是和std::shared_ptr<std::string>,则将发生类似的错误:

previous_message = 0;

为了清除消息,我们说:

previous_message.clear();

症状是意外发送空消息,而不是不确定的行为。额外复制非常大的字符串的成本可能比复制a的成本要重要得多shared_ptr,因此权衡可能会有所不同。


16
传入的shared_ptr已经存在于调用站点的作用域中。您也许可以创建一个复杂的场景,该问题中的代码由于悬空的指针而崩溃,但是我想您的问题要比参考参数大!
马格纳斯·霍夫

10
它可以存储在成员中。您可以打电话碰巧清除该成员。smart_ptr的全部要点是避免必须协调嵌套在调用堆栈周围的层次结构或范围中的生命周期,因此这就是为什么最好假设生命周期在此类程序中不做的事情。
Daniel Earwicker's

7
不过,这并不是我真正的观点!如果您认为我的意思是与我的代码有关的,那么您可能不了解我。我首先要说的是shared_ptr存在的原因的不可避免的含义:许多对象生存期不仅仅与函数调用有关。
Daniel Earwicker's

8
@DanielEarwicker完全同意您的所有观点,并对反对的程度感到惊讶。使您的担忧更加相关的是线程,当涉及到线程时,关于对象有效性的保证就变得更加重要。好答案。
radman

3
不久前,我解决了一个非常严重的错误,该错误是由于将引用传递给共享指针而导致的。该代码正在处理对象的状态更改,当它注意到对象的状态已更改时,便将其从先前状态的对象集合中删除,然后将其移至新状态的对象集合中。删除操作破坏了该对象的最后一个共享指针。在对集合中共享指针的引用上调用了成员函数。繁荣。Daniel Earwicker是对的。
David Schwartz 2013年

115

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

赫伯·萨特:“当您通过shared_ptrs时,副本很昂贵”

斯科特·迈耶斯(Scott Meyers):“无论是通过值传递还是通过引用传递,shared_ptr都没什么特别的。使用与其他用户定义类型完全相同的分析。人们似乎有一种感觉,shared_ptr可以解决所有管理问题,而且因为它很小,所以按价值传递的成本一定很便宜,必须复制,并且要付出一定的代价……按价值传递,这很昂贵,所以如果我能摆脱困境它在我的程序中具有适当的语义,我将通过引用将其传递给const或引用”

赫伯·萨特(Herb Sutter):“总是通过引用将它们传递给const,有时可能是因为您知道调用的内容可能会修改您从引用中获得的内容,也许然后您可能会按值传递...如果将它们复制为参数,哦天哪,您几乎不需要增加引用计数,因为无论如何它都会被保存,您应该通过引用传递它,所以请这样做”

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


8
好发现!很高兴看到两位最重要的专家公开驳斥了有关SO的传统观点。
Stephan Tolksdorf'2

3
“对于无论是通过值传递还是通过引用传递,shared_ptr都没有什么特别的” –我真的不同意。很特别 就我个人而言,我宁愿放心使用它,并承受轻微的性能损失。如果需要优化某个特定的代码区域,那么可以肯定,我将看看const ref传递shared_ptr的性能优势。
JasonZ'2

3
有趣的是,尽管就过度使用shared_ptrs达成了共识,但对于传递值与引用问题并没有达成共识。
Nicol Bolas '04年

2
斯科特·迈耶斯(Scott Meyers):“因此,如果我能在程序中使用适当的语义来摆脱它……”,那根本不与我的回答相矛盾,这指出弄清楚是否将参数更改为const &会影响语义只是非常简单的事情。简单的程序。
Daniel Earwicker 2012年

7
赫伯·萨特(Herb Sutter):“有时可能是因为您知道您所说的内容可能会修改您从中获得参考的内容”。再说一次,对小案件的一点豁免,所以这与我的回答并不矛盾。问题仍然存在:您如何知道使用const ref是安全的?在简单的程序中很容易证明,而在复杂的程序中则不那么容易。但是,嘿,这 C ++,所以我们比所有其他工程技术都更喜欢过早的微优化,对吧?:)
Daniel Earwicker 2012年

22

我建议您不要这样做,除非您和与您一起工作的其他程序员真的非常了解自己在做什么。

首先,您不知道与类的接口将如何演变,并且您想防止其他程序员做坏事。通过引用传递shared_ptr并不是程序员应该期望看到的,因为它不是惯用语言,因此很容易错误地使用它。防御性编程:使界面难以正确使用。通过引用传递只是以后会引起问题。

其次,在您知道该特定类将成为问题之前,请不要进行优化。首先进行概要分析,然后再分析,如果您的程序确实需要通过引用传递来增强性能,那么也许可以。否则,不要费力小事(即按值传递需要花费额外的N条指令),而不必担心设计,数据结构,算法和长期可维护性。


尽管从技术上来说,litb的回答是正确的,但切勿低估程序员的“懒惰”(我也很懒!)。littlenag的答案更好,因为对shared_ptr的引用将是意外的,并且可能(可能)是不必要的优化,这会使将来的维护更具​​挑战性。
2008年

18

是的,在那里参考是可以的。您无意授予该方法共享所有权;它只想使用它。对于第一种情况,您也可以参考,因为无论如何都要复制它。但是对于第一种情况,它需要所有权。有一个技巧仍然只能复制一次:

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

返回时也应进行复制(即不返回引用)。因为您的类不知道客户端在做什么(它可以存储指向它的指针,然后发生大爆炸)。如果以后发现这是一个瓶颈(第一个配置文件!),那么您仍然可以返回引用。


编辑:当然,正如其他人指出的那样,仅当您知道您的代码并且知道您不以某种方式重置传递的共享指针时,这才是正确的。如有疑问,请按值传递。


11

通过shared_ptrs 是明智的const&。它不太可能引起麻烦(除非在不太可能的情况下,引用的引用shared_ptr在函数调用期间被删除(如Earwicker所述),并且如果您将许多此类问题传递给别人,它可能会更快。记得; 默认boost::shared_ptr值为线程安全,因此复制它包括线程安全增量。

尝试使用const&而不是仅仅&,因为非const引用可能不会传递临时对象。(即使MSVC中的语言扩展允许您仍然这样做)


3
是的,我一直使用const引用,只是忘了将其放在示例中。无论如何,MSVC允许将非常量引用绑定到临时对象,而不是为了解决错误,但因为默认情况下,它具有将属性“ C / C ++->语言->禁用语言扩展”设置为“否”。启用它并不会编译它们...
abigagli

abigagli:认真吗?甜!我将在工作中强制执行此操作,明天是第一件事;)
Magnus Hoff

10

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

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

您可以将其称为

only_work_with_sp(*sp);

3
如果您在不需要复制指针的副本时采用约定来使用对象引用,则它也可以用来记录您的意图。它还为您提供了使用const引用的机会。
Mark Ransom

是的,我同意使用对对象的引用作为表示被调用函数没有“记住”该对象的任何内容的手段。通常,如果函数是对象的“跟踪”,我通常使用指针形式参数
abigagli

3

除非函数明确地修改了指针,否则我将避免使用“普通”引用。

const &调用小功能时-A 可能是明智的微优化-例如,实现进一步的优化(如内联某些条件)。另外,由于线程安全,因此增量/减量是一个同步点。不过,我不希望这在大多数情况下都会有很大的不同。

通常,除非有理由不要使用简单的样式。然后,要么const &一致地使用,要么添加注释,说明为什么仅在几个地方使用它。


3

我主张通过const引用传递共享指针-一种语义,即与指针一起传递的函数不拥有该指针,这对于开发人员来说是一种干净的习惯用法。

唯一的陷阱是在多个线程程序中,共享指针指向的对象在另一个线程中被破坏。因此可以肯定地说在单线程程序中使用共享指针的const引用是安全的。

通过非const引用传递共享指针有时很危险-原因是该函数可能在内部调用的swap和reset函数,以销毁在函数返回后仍视为有效的对象。

我想,这不关乎过早的优化-它的目的是避免在您清楚要做什么并且开发人员已经坚决采用了编码习惯时避免不必要的CPU周期浪费。

只是我的2美分:-)


1
请参阅David Schwartz的上述评论:“ ...我追赶了一个非常严重的错误,该错误是由于将引用传递给共享指针而引起的。该代码正在处理对象的状态更改,并且当它注意到对象的状态已更改时,它从先前状态的对象集合中删除了它,并将其移至新状态的对象集合中; remove操作破坏了指向该对象的最后一个共享指针。成员函数是在对共享指针的引用上调用的在集合中。” ...
杰森·哈里森

3

似乎这里所有的优缺点实际上都可以推广到通过引用传递的任何类型,而不仅仅是shared_ptr。我认为,您应该了解按引用传递,const引用和值传递的语义,并正确使用它。但是通过引用传递shared_ptr绝对没有错,除非您认为所有引用都是错误的...

回到示例:

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

您怎么知道sp.do_something()不会因指针悬空而炸毁?

事实是,无论您是否有设计缺陷,例如是否sp在线程之间直接或间接共享线程的所有权,滥用do的对象,delete this存在循环所有权或其他所有权错误,无论是否使用shared_ptr,都可能会发生这种情况。


2

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

例如,此代码将产生一个错误,但是如果您进行更改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;
}

1

我假设您熟悉过早的优化并出于学术目的或因为您隔离了一些性能欠佳的现有代码而提出了此要求。

通过引用可以

通过const引用传递更好,通常可以使用,因为它不会在所指向的对象上强制保持恒定。

不会因为使用引用而失去丢失指针的风险。该参考证明您在堆栈中的较早位置具有智能指针的副本,并且只有一个线程拥有调用堆栈,因此预先存在的副本不会消失。

由于您提到的原因,使用引用通常会更有效,但不能保证。请记住,取消引用对象也可以进行工作。理想的参考使用场景是,如果您的编码风格涉及许多小函数,则在使用之前,指针将在函数之间传递。

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


1
“您不会因为使用引用而失去丢失指针的风险。该引用证明您在堆栈中的较早位置具有智能指针的副本”或数据成员...?
Daniel Earwicker's

考虑一下boost :: thread和boost :: ref:boost :: function <int> functionPointer = boost :: bind(doSomething,boost :: ref(sharedPtrInstance));的魔力。m_workerThread =新的boost :: thread(functionPointer); ...删除sharedPtrInstance
杰森·哈里森

1

假设我们不关心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重置原始指针,这会将其使用计数减少为0,从而破坏了其数据。但是,FooTakesValue无法重置原始指针,从而保证了sharedB的数据仍然可用。当另一位开发人员不可避免地出现并试图shared积sharedA的脆弱存在时,就会出现混乱。但是,幸运的sharedB开发人员会尽早回家,因为他的世界万事大吉

在这种情况下,代码安全性远远超过了复制所创造的任何速度改进。同时,boost :: shared_ptr旨在提高代码安全性。如果某些东西需要这种利基优化,从副本到参考文献将容易得多。


1

桑迪写道:“看来这里的所有优缺点实际上都可以推广到通过引用传递的任何类型,而不仅仅是shared_ptr。”

在某种程度上是正确的,但是使用shared_ptr的目的是消除与对象生存期有关的问题,并让编译器为您处理。如果要通过引用传递共享指针,并允许引用计数对象的客户端调用可能释放对象数据的非const方法,则使用共享指针几乎是没有意义的。

我在前一句话中写了“几乎”,因为性能可能是个问题,在极少数情况下“可能”是合理的,但我自己也避免这种情况,自己寻找所有可能的其他优化解决方案,例如认真考虑在添加另一个级别的间接,惰性评估等方面。

超出其作者甚至存在于其内存中的代码,需要对行为的隐式假设,尤其是有关对象生命周期的行为,需要清晰,简洁,可读的文档,然后许多客户还是不会阅读它!简单性几乎总是胜过效率,并且几乎总是有其他方法可以提高效率。如果您确实需要通过引用传递值,以避免引用计数对象(和equals运算符)的副本构造函数进行深度复制,那么也许您应该考虑使深度复制的数据成为引用计数指针的方式,这些指针可以快速复制。(当然,这只是一种设计方案,可能不适用于您的情况)。


1

我曾经在一个项目中工作过,该原则非常强调按值传递智能指针。当我被要求做一些性能分析时,我发现对于智能指针的参考计数器的递增和递减,应用程序花费了所用处理器时间的4-6%。

如果您要按值传递智能指针,只是为了避免在丹尼尔·艾威克(Daniel Earwicker)所述的奇怪情况下出现问题,请确保您了解所付出的代价。

如果您决定使用引用,则使用const引用的主要原因是当您需要将共享指针传递给继承了在接口中使用的类的类的对象时,可以进行隐式向上转换。


0

除了litb所说的以外,我想指出的是第二个示例中可能要通过const引用来传递,这样可以确保您不会意外修改它。


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

0

每个代码段都必须具有某种意义。如果您在应用程序中的任何地方按值传递共享指针,则意味着“我不确定其他地方发生了什么,因此我赞成原始安全性 ”。对于那些可以查询该代码的其他程序员,这不是我所说的良好信心。

无论如何,即使函数获得const引用并且您“不确定”,您仍然可以在函数的开头创建共享指针的副本,以为指针添加强引用。这也可以看作是有关设计的提示(“可以在其他地方修改指针”)。

因此,对,IMO,默认值应为“ 通过const引用传递 ”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.