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

时间:2008-11-29 14:27:31

标签: c++ boost pass-by-reference pass-by-value 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>的充分理由。按值传递只会“暂时”增加由于复制而引用的引用计数,然后在退出函数范围时减少它。 我忽略了什么吗?

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

17 个答案:

答案 0 :(得分:111)

我发现自己不同意最高投票的答案,所以我去找专家意见,他们就是这样。 来自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“除非你想使用或操纵智能指针本身,例如分享或转让所有权。“

答案 1 :(得分:107)

不同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. 将参数更改为不同的对象而不是引用。

  3. 适用于此处的一般建议:为了提高性能,不要费心对代码进行风险更改,直到您在分析器中将实际情况计时并最终确定您要进行的更改为止将对绩效产生重大影响。

    评论者JQ的更新

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

    我们有一个能在某处发送消息的功能。它可能是一条很大的消息,所以我们使用std::string来代替字符串,而不是使用可能会被复制的shared_ptr,而不是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!

    正如预期的那样,这会打印send_message两次。

    现在,Maintainer先生来看看代码并思考:嘿,shared_ptr的参数是void send_message(std::shared_ptr<std::string> msg)

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

    显然可以改为:

    send_message

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

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

    Maintainer先生对此感到惊讶,但为void send_message(const std::shared_ptr<std::string> &msg) { if (msg == 0) return; 添加了防御性检查以试图阻止问题的发生:

    msg

    但当然它仍然会继续崩溃,因为send_message在调用shared_ptr时永远不会为空。

    正如我所说,所有代码在一个简单的例子中如此接近,很容易找到错误。但是在实际程序中,在可变对象之间存在更复杂的关系,这些对象之间存在指针,很容易错误,并且很难构建必要的测试用例来检测错误。

    简单的解决方案,您希望函数能够依赖shared_ptr始终为非null,是为了让函数分配自己的真shared_ptr,而不是依赖对现有shared_ptr的引用。

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

    请注意,如果我们使用std::shared_ptr<std::string>代替previous_message = 0; 而不是previous_message.clear(); ,则会发生类似的错误,而不是:

    shared_ptr

    要清除消息,我们说:

    {{1}}

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

答案 2 :(得分:22)

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

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

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

答案 3 :(得分:18)

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

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

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


编辑:当然,正如其他人所指出的那样,只有在您知道自己的代码并且知道不以某种方式重置传递的共享指针时才会出现这种情况。如果有疑问,只需按值传递。

答案 4 :(得分:11)

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

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

答案 5 :(得分:10)

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

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

您可以将其称为

only_work_with_sp(*sp);

答案 6 :(得分:3)

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

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

通常,除非您有理由,否则应使用更简单的样式。然后,要么始终如一地使用const &,要么添加注释,以便在一些地方使用它。

答案 7 :(得分:3)

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

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

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

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

只需2美分: - )

答案 8 :(得分:3)

似乎这里的所有优点和缺点实际上可以推广到通过引用而不仅仅是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与否,如果您有一个设计缺陷,例如直接或间接地在线程之间共享sp的所有权,而不是使用{{1你有循环所有权或其他所有权错误。

答案 9 :(得分: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;
}

答案 10 :(得分: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的数据仍然可用。当另一位开发人员不可避免地出现并试图依赖 sharedA的脆弱存在时,随之而来的是混乱。然而,幸运的 sharedB 开发者很早就回家了,因为他的世界都是正确的。

在这种情况下,代码安全性远远超过复制所带来的任何速度提升。同时,boost :: shared_ptr旨在提高代码安全性。如果某些东西需要这种小众优化,那么从副本到参考将会容易得多。

答案 11 :(得分:1)

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

如果您想通过值传递智能指针,只是为了避免在Daniel Earwicker中描述的奇怪案例中出现问题,请确保您了解为此付出的代价。

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

答案 12 :(得分:1)

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

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

我在前一句中写了“差不多”,因为性能可能是一个问题,并且在极少数情况下它可能是合理的,但我也会自己避免这种情况并自己寻找所有可能的其他优化解决方案,例如要认真考虑增加另一层次的间接,懒惰的评价等。

存在于作者之前的代码,或者甚至发布作者的内存,需要隐含的行为假设,特别是关于对象生命周期的行为,需要清晰,简洁,可读的文档,然后许多客户端无论如何都不会阅读它!简单性几乎总是胜过效率,而且几乎总有其他方法可以提高效率。如果你真的需要通过引用传递值来避免你的引用计数对象(和equals运算符)的拷贝构造函数进行深度复制,那么你可能应该考虑一些方法来使深度复制的数据成为可以引用的引用计数指针快速复制。 (当然,这只是一种可能不适用于您的情况的设计方案。)

答案 13 :(得分:1)

我会假设您熟悉premature optimization并且要求将其用于学术目的,或者因为您已经隔离了一些效果不佳的预先存在的代码。

通过引用传递是可以的

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

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

根据您提及的原因常常更有效但不保证。请记住,取消引用对象也可以起作用。理想的参考用法场景是,如果你的编码风格涉及许多小函数,那么指针在使用之前会从函数传递到函数。

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

答案 14 :(得分: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;
    }
    

答案 15 :(得分:0)

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

答案 16 :(得分:0)

每个代码都必须具备一定的意义。如果您在应用程序中通过值无处不在传递共享指针,这意味着&#34;我不确定其他地方发生了什么,因此我赞成原始安全&#34 ;.这不是我称之为可以查阅代码的其他程序员的良好信任标志。

无论如何,即使一个函数获得一个const引用并且你不确定&#34;,你仍然可以在函数的头部创建一个共享指针的副本,以添加对指针的强引用。这也可以看作是关于设计的暗示(&#34;指针可以在别处修改&#34;)。

所以,是的,IMO,默认应该是&#34; 通过const引用&#34;。