116

如果我有一个需要使用 a 的函数,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>值而不是引用的充分理由。按值传递只会“暂时”增加由于复制而导致的引用计数,然后在退出函数范围时减少它。我忽略了什么吗?

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

4

17 回答 17

117

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

Herb Sutter:“当你通过 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 完全“除非您想使用或操纵智能指针本身,例如共享或转移所有权。”

于 2012-01-13T01:48:22.337 回答
114

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

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

因此,通过使用对 a 的引用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 的更新

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

我们有一个函数可以在某处发送消息。它可能是一条大消息,因此我们使用 a到一个字符串,而不是使用std::string可能在传递到多个地方时被复制的a :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是 a shared_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时永远不会为空send_message

正如我所说,在一个简单的示例中,所有代码都如此紧密地结合在一起,很容易找到错误。但在实际程序中,相互持有指针的可变对象之间的关系比较复杂,很容易出错,很难构建必要的测试用例来检测错误。

一个简单的解决方案是,您希望函数能够始终依赖于shared_ptr非空值,即函数分配其自己的 true shared_ptr,而不是依赖于对现有 的引用shared_ptr

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

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

previous_message = 0;

为了清除信息,我们说:

previous_message.clear();

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

于 2008-11-30T10:38:40.140 回答
23

我建议不要这种做法,除非你和其他与你一起工作的程序员真的、真的知道你们都在做什么。

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

其次,在您知道这个特定的类将成为问题之前不要进行优化。首先配置文件,然后如果您的程序确实需要通过引用传递来提供提升,那么也许。否则,不要为小事(即通过值传递所需的额外 N 条指令)操心,而是担心设计、数据结构、算法和长期可维护性。

于 2008-11-29T16:32:32.390 回答
18

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

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

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


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

于 2008-11-29T14:34:29.993 回答
11

通过shared_ptrs 是明智的const&。它不太可能引起麻烦(除非在shared_ptr函数调用期间删除了引用的极少数情况,如 Earwicker 所详述),并且如果您传递大量这些,它可能会更快。记住; 默认boost::shared_ptr是线程安全的,所以复制它包括一个线程安全的增量。

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

于 2008-11-30T14:57:43.750 回答
10

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

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

你可以把它称为

only_work_with_sp(*sp);
于 2008-11-29T16:21:54.410 回答
3

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

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

通常,除非您有理由不这样做,否则您应该使用更简单的样式。然后,要么使用const &一致,要么添加评论,说明为什么你只在几个地方使用它。

于 2008-11-29T19:12:27.780 回答
3

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

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

通过非常量引用传递共享指针有时是危险的 - 原因是函数可能在内部调用的交换和重置函数以破坏函数返回后仍然被认为有效的对象。

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

只是我的 2 美分 :-)

于 2010-03-28T19:37:28.183 回答
3

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

回到示例:

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

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

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

于 2010-11-29T00:02:27.557 回答
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;
}
于 2014-09-25T22:56:21.833 回答
1

我假设您熟悉过早优化并且出于学术目的或因为您已经隔离了一些性能不佳的预先存在的代码而提出此问题。

通过引用传递是可以的

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

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

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

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

于 2008-11-30T16:00:36.107 回答
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 旨在提高代码安全性。如果某些事情需要这种利基优化,那么从副本到参考会容易得多。

于 2010-03-03T14:24:51.610 回答
1

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

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

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

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

于 2011-09-02T19:46:53.760 回答
1

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

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

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

于 2013-04-11T16:30:04.930 回答
0

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

于 2008-11-29T17:36:21.153 回答
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;
    }
    
于 2013-07-23T07:23:47.450 回答
0

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

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

所以是的,IMO,默认值应该是“通过 const 引用传递”。

于 2016-03-05T07:45:33.510 回答