C + +-传递对 std: : share_ptr 或 Boost: : share_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>。通过值传递只会由于复制而“临时”增加引用计数,然后在退出函数范围时减少引用计数。 我是不是忽略了什么?

只是澄清一下,在阅读了几个答案之后: 我完全同意过早优化的担忧,我总是试图先分析,然后在热点工作。如果你明白我的意思,我的问题更多的是从纯技术代码的角度出发。

53208 次浏览

Yes, taking a reference is fine there. You don't intend to give the method shared ownership; it only wants to work with it. You could take a reference for the first case too, since you copy it anyway. But for first case, it 需要 ownership. There is this trick to still copy it only once:

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

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


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

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

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

你可以称之为

only_work_with_sp(*sp);

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

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

第二,在你知道这个特定的类会成为一个问题之前,不要进行优化。首先分析,然后如果您的程序确实需要通过引用传递来提高,那么可能。否则,不要担心那些小事情(例如,通过值传递需要额外的 N 条指令) ,而是担心设计、数据结构、算法和长期可维护性。

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

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

在调用小函数时,const &可能是一个明智的微型优化——例如,启用进一步的优化,比如内联去掉某些条件。另外,增量/减量(因为它是线程安全的)是一个同步点。不过,在大多数情况下,我并不指望这会产生重大影响。

一般来说,你应该使用简单的样式,除非你有理由不这样做。然后,要么一致地使用 const &,要么添加一个注释,说明为什么只在几个地方使用它。

The point of a distinct shared_ptr instance is to guarantee (as far as possible) that as long as this shared_ptr is in scope, the object it points to will still exist, because its reference count will be at least 1.

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

So by using a reference to a shared_ptr, you disable that guarantee. So in your second case:

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. Examine the source of your entire program very carefully until you are sure the object won't die during the function body.

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

General bit of advice that applies here: don't bother making risky changes to your code for the sake of performance until you've timed your product in a realistic situation in a profiler and conclusively measured that the change you want to make will make a significant difference to performance.

Update for commenter JQ

Here's a contrived example. It's deliberately simple, so the mistake will be obvious. In real examples, the mistake is not so obvious because it is hidden in layers of real detail.

我们有一个函数,将发送消息的地方。它可能是一个很大的消息,所以我们不使用 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!

现在来了 Maintainer 先生,他看着代码想: 嘿,send_message的参数是 shared_ptr:

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

显然,这可以改为:

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

想想这将带来的性能提升吧!(不要介意我们将要通过某个通道发送一个典型的大消息,因此性能增强将非常小以至于无法测量)。

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

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

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

但是当然它仍然会继续运行并崩溃,因为当 send_message被调用时,msg永远不会为空。

As I say, with all the code so close together in a trivial example, it's easy to find the mistake. But in real programs, with more complex relationships between mutable objects that hold pointers to each other, it is easy to 制造 the mistake, and hard to construct the necessary test cases to detect the mistake.

The easy solution, where you want a function to be able to rely on a shared_ptr continuing to be non-null throughout, is for the function to allocate its own true shared_ptr, rather than relying on a reference to an existing shared_ptr.

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

注意,如果我们全程使用 std::string而不是 std::shared_ptr<std::string>,而不是:

previous_message = 0;

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

previous_message.clear();

那么症状就是意外地发送了一条空消息,而不是未定义的行为。一个非常大的字符串的额外副本的成本可能比复制一个 shared_ptr的成本要大得多,因此折衷方案可能是不同的。

It is sensible to pass shared_ptrs by const&. It will not likely cause trouble (except in the unlikely case that the referenced shared_ptr is deleted during the function call, as detailed by Earwicker) and it will likely be faster if you pass a lot of these around. Remember; the default boost::shared_ptr is thread safe, so copying it includes a thread safe increment.

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

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

通过引用是可以的

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

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

使用引用是 经常更有效的原因,你提到的,but not guaranteed。请记住,解除对象引用也可能需要工作。理想的引用使用场景是,如果您的编码风格涉及许多小函数,在使用之前,指针将从一个函数传递到另一个函数。

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

假设我们不关心常量正确性(或者更多,您的意思是允许函数能够修改或共享传入数据的所有权) ,传递一个提升: : share _ ptr by value 比通过引用传递它更安全,因为我们允许原始的提升: : share _ 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无法重置原始指针,保证 共享的 B数据仍然可用。当另一个开发人员不可避免地出现并试图利用 共享的 A脆弱的存在时,混乱随之而来。然而,这位幸运的 共享 B开发人员很早就回家了,因为他的世界一切正常。

在这种情况下,代码安全性远远超过复制速度的提高。同时,升级: : share _ ptr 意味着提高代码安全性。如果有些事情需要这种小众优化,那么从一份拷贝到一份参考就会容易得多。

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

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

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

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

只是我的2分钱: -)

看起来,这里的所有优缺点实际上都可以推广到任何通过引用传递的类型,而不仅仅是 share _ ptr。笔者认为,要正确认识和运用参照传递、常参照传递和价值传递的语义。但是通过引用传递 share _ ptr 绝对没有本质上的错误,除非您认为所有引用都是错误的..。

回到这个例子:

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

你怎么知道 abc0不会因为迷途指针而爆炸?

事实是,不管你是否共享 _ ptr,const 或者不共享,如果你有一个设计缺陷,比如直接或间接地在线程之间共享 sp的所有权,错误地使用了一个执行 delete this的对象,你有一个循环所有权或者其他的所有权错误,这都可能发生。

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

在某种程度上是正确的,但是使用 share _ ptr 的目的是消除对象生命周期的担忧,并让编译器为您处理这个问题。如果你打算通过引用传递一个共享指针,并允许客户端的引用计数对象调用可能释放对象数据的 non-const 方法,那么使用一个共享指针几乎是毫无意义的。

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

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

我发现自己不同意票数最高的答案,所以我去寻找专家意见,他们在这里。 来自 href = “ http://channel el9.msdn.com/Shows/going + deep/c-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-anything”rel = “ nofollow noReferrer”> http://channel9.msdn.com/shows/going+deep/c-and-beyond-2011-scott-andrei-and-herb-ask-us-anything

Herb Sutter: "when you pass shared_ptrs, copies are expensive"

Scott Meyers: “无论是通过值传递还是通过引用传递,share _ ptr 都没有什么特别之处。使用与任何其他用户定义类型完全相同的分析。人们似乎认为 share _ ptr 以某种方式解决了所有的管理问题,并且因为它很小,所以通过值传递它必然是廉价的。它必须被复制,并且有相关的成本... 通过值传递它是昂贵的,所以如果我可以在我的程序中使用正确的语义,我将通过引用传递它到 const 或者 reference。”

Herb Sutter: “总是通过引用 const 来传递它们,偶尔可能是因为你知道你调用的东西可能会修改你得到的引用,然后你可能会通过值来传递... ... 如果你把它们作为参数来复制,哦,我的上帝,你几乎不需要改变引用计数,因为它是活的,你应该通过引用来传递它,所以请这样做。”

更新: Herb 在这里对此进行了扩展: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/,尽管这个故事的寓意是你根本不应该传递 share _ ptrs,“除非你想使用或操纵智能指针本身,比如共享或转移所有权。”

我曾经在一个项目中工作,这个项目的原则是通过值来传递智能指针。当我被要求做一些性能分析时——我发现,对于智能指针的引用计数器的增量和减量,应用程序花费的处理器时间约为所用时间的4-6% 。

如果您希望通过值传递智能指针,只是为了避免像 Daniel Earwicker 描述的那样在奇怪的情况下出现问题,那么请确保您理解为此付出的代价。

If you decide to go with a reference the main reason to use const reference is to make it possible to have implicit upcasting when you need to pass shared pointer to object from class that inherits the class you use in the interface.

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. pass by reference:

    void set(shared_ptr<Message>& msg) {
    this->msg = msg; /// reference count will be added, because reference is just an alias.
    }
    
  3. pass by pointer:

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

我还没有提到的一点是,当通过引用传递共享指针时,如果希望通过对基类共享指针的引用传递派生类共享指针,就会失去所获得的隐式转换。

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

每一段代码都必须有意义。如果在应用程序中通过值 无处不在传递共享指针,这意味着 "I am unsure about what's going on elsewhere, hence I favour raw safety”。对于其他可以查阅代码的程序员来说,这不是我所说的好的信心标志。

无论如何,即使一个函数得到一个常量引用,而你“不确定”,你仍然可以在函数的头部创建一个共享指针的副本,为指针添加一个强引用。这也可以被看作是关于设计的暗示(“指针可以在其他地方修改”)。

所以,是的,在我看来,默认值应该是“ 通过常量引用传递”。