C++ - 传递对 std::shared_ptr 或 boost::shared_ptr 的引用
-
11-07-2019 - |
题
如果我有一个需要与 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>
按值而不是按引用。按值传递只会因复制而“暂时”增加引用计数,然后在退出函数作用域时减少引用计数。我是否忽略了什么?
只是为了澄清,在阅读了几个答案后:我完全同意过早优化的担忧,并且我总是尝试先分析热点,然后再处理热点。如果你明白我的意思的话,我的问题更多地是从纯粹的技术代码角度来看的。
解决方案
一个独特的点 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
}
因此,通过使用对 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
到那个物体?再见对象,就在您将要尝试使用它的地方。
所以有两种方法可以回答这个问题:
非常仔细地检查整个程序的源代码,直到确定对象不会在函数体期间消亡。
将参数更改回不同的对象而不是引用。
适用于此处的一般建议:不要为了性能而对代码进行冒险的更改,除非您在分析器中的实际情况下对产品进行了计时,并最终测量到您想要进行的更改将对性能产生显着影响。
评论者 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
是一个 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
.
缺点是复制了一个 shared_ptr
不是免费的:即使“无锁”实现也必须使用互锁操作来遵守线程保证。因此,在某些情况下,可以通过更改 shared_ptr
变成一个 shared_ptr &
. 。但这并不是可以对所有程序安全地进行的更改。它改变了程序的逻辑含义。
请注意,如果我们使用,也会出现类似的错误 std::string
整个而不是 std::shared_ptr<std::string>
, ,而不是:
previous_message = 0;
为了清除该消息,我们说:
previous_message.clear();
那么症状将是意外发送空消息,而不是未定义的行为。额外复制一个非常大的字符串的成本可能比复制一个字符串的成本要大得多 shared_ptr
, ,因此权衡可能会有所不同。
其他提示
我发现自己与-投票最高的答案不同意,所以我去寻找专家很有高见,并在这里他们。 从 http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything
香草萨特: “当你通过的shared_ptr的,拷贝是昂贵的”
斯科特迈尔斯:“没有什么特别的shared_ptr的,当谈到是否按值传递,或按引用传递它的人似乎有这种看法使用您所使用的任何其它用户定义的类型相同的分析认为。 shared_ptr的某种方式解决所有的管理问题,这是因为它的小,这是必然廉价按值传递,它必须被复制,并且有与相关的成本......它的价格昂贵按值传递的,所以如果我能在我的程序正确的语义摆脱它,我要传参照const或引用,而不是“
香草萨特:“总是被引用为const通过他们,非常偶然,也许因为你知道你可以叫你修改了从一个参考的东西是什么,也许你可能会按值传递......如果你复制他们作为参数,我的天哪,你几乎从不需要,因为它正在举行活着反正碰到的是引用计数,你应该用引用传递,所以请不要说“
更新:赫伯已经在这个位置展开: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ ,虽然故事的寓意是,你不应该传递的shared_ptr的所有“除非要使用或操作的智能指针本身,如共享或转让所有权。“
我会建议反对这种做法,除非你和其他程序员,你用真功夫,的真正的我知道你们都在做什么。
首先,你不知道怎样的接口类可能演变并要防止其他程序员从做坏事。按引用传递shared_ptr的是不是一个程序员应该期望看到的,因为它不是惯用的,这使得它很容易错误地使用它。项目防守:使界面难以错误使用。通过引用传递只是要邀请以后的问题。
二,不优化,直到你知道这个特定的类将是一个问题。简介第一,然后如果你的程序确实需要通过引用传递给升压,然后可能。否则,不出汗的小的东西(即,它需要通过值传递额外的N个指令),而不是担心设计,数据结构,算法,和长期的可维护性。
是,考虑的参考是细那里。你不打算给方法共享所有权;它只想与它合作。你可以采取第一种情况下的参考过,因为你无论如何复制。但对于第一种情况下,它的需要的所有权。有这一招还是复制它只有一次:
void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
m_sp_member.swap(sp);
}
您也应该复制,当您返回它(即不返回的引用)。因为你的类不知道什么客户端用它做(它可以存储指向它,然后大爆炸发生的)。如果后来证明这是一个瓶颈(第一个人资料!),那么你仍然可以返回引用。
修改:当然,正如其他人指出,这只是如果你知道你的代码,知道你不重置通过共享指针在某种程度上是真实的。如有疑问,只是通过值传递。
这是明智通过shared_ptr
传递const&
s。它不太可能造成麻烦(除了在被引用shared_ptr
被调用函数中删除,由埃里克详见不太可能的情况下),它很可能会更快,如果你通过周围很多这样的。记得;默认boost::shared_ptr
是线程安全,所以复制它包括一个线程安全增量。
尝试使用const&
而不仅仅是&
,因为临时对象可以不被非const引用传递。 (即使在MSVC语言扩展可以让你做吧)
在第二种情况下,这样做是更简单的:
Class::only_work_with_sp(foo &sp)
{
...
sp.do_something();
...
}
可以将其称为
only_work_with_sp(*sp);
我将避免“纯”指代,除非明确地函数可以修改指针。
例如 -调用小函数当A const &
可以是明智的微优化使进一步的优化,内联一样离开一定的条件。此外,递增/递减 - 因为它是线程安全的 - 是一个同步点。我不希望这样做在大多数情况下有很大的不同,虽然。
一般情况下,你应该除非你有理由不使用简单的样式。然后,要么使用const &
一致,或添加评论,为什么,如果你使用它只是在几个地方。
我会主张通过const引用传递共享指针 - 一个语义,该函数与所述指针传递不拥有该指针,这是为开发清洁成语
在唯一的缺陷是在多个线程程序被指向由共享指针被在另一个线程被破坏的对象。所以它是安全的使用共享指针的const引用是在单线程程序安全地说。
传共享由非const引用指针有时是危险的。 - 的原因是交换和复位功能的功能可以调用内部以便破坏其功能返回后仍然被认为是有效的对象
这是不是过早的优化,我想 - 它是如何避免CPU周期的不必要的浪费,当你清楚自己想要做什么和编码成语已经牢牢被采纳您的伙伴开发
只是我的2美分: - )
似乎所有的优点和缺点在这里其实可以推广到任何类型的引用不只是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
的所有权,missusing的对象做delete this
,你有一个环形的所有权,这可能发生或其他所有权错误。
这是我还没有看到提到的一件事是,当你通过引用传递共享指针,你,如果你想通过基类共享指针引用传递一个派生类共享指针丢失,你得到的隐式转换
例如,这个代码将产生错误,但如果你改变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;
}
我假设你熟悉过早的优化和所要求这无论是学术目的或因为你已经分离了一些预先存在的代码,效果不佳。
通过引用传递是可以的
通过const引用传较好,并且通常可以使用,因为它不会强制常量性上指向的对象。
您是的不是在丢失指针由于使用参考的风险。这参考证据证明你有智能指针的副本早些时候在堆栈只有一个线程拥有的调用堆栈,使已存在的副本是不会消失的。
使用引用是往往更有效地为你所提到的原因,请,但不能保证。请记住,访问一个对象可以工作了。如果您的编码风格包含许多小功能,在指针会得到从功能传递给函数在使用前功能您的理想参考,使用场景会。
您应该始终避免存储您的智能指针作为参考。您的Class::take_copy_of_sp(&sp)
示例显示了正确的使用。
假设我们不关心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 的开发商,但是,回家早,因为所有是正确的,他的世界。
代码的安全性,在这种情况下,远远超过任何速度改善复印创建。与此同时,升压:: shared_ptr的是为了提高代码的安全性。它会很容易从一个副本去一个参考,如果有什么需要这种小众的优化。
桑迪写道:“看来,这里所有的优点和缺点其实是可以推广到任何类型的引用不只是shared_ptr的通过”
真在一定程度上,但使用的shared_ptr的点是消除关于对象寿命的关注和让编译器句柄给你。如果你想通过引用传递一个共享的指针,并允许可能释放对象的数据,然后使用一个共享指针你引用计数的对象调用非const方法的客户几乎是毫无意义的。
我写了“几乎”,在前面的句子,因为性能是一个问题,而且它可能会“在极少数情况下是合理的,但我也想避免这种情况我自己,寻找所有可能的其他优化解决方案我自己,如认真看加入间接,懒惰评估等的另一个水平..
这是存在的过去,它的作者,甚至张贴它的作者的记忆中,这大约需要行为隐含的假设,有关对象生命周期特定行为守则,要求清晰,简洁,可读的文档,然后很多客户无论如何也不会读它!简单几乎总是胜过效率,几乎总有其他的方法是有效的。如果你真的需要按引用传递值由您的引用计数的对象的拷贝构造函数,以避免深度复制(和等于运算),那么也许你应该考虑如何使深拷贝的数据是引用计数指针,可以是迅速复制。 (当然,这只是可能并不适用于您的情况一个设计方案)。
我以前在一个项目的原则是大约按值传递智能指针非常强的工作。当我被要求做一些性能分析 - I发现,对于智能指针应用程序花费的利用处理器时间4-6%之间
的引用计数器的递增和递减。如果您希望通过值传递的智能指针只是为了避免从丹尼尔埃里克描述具有怪异的情况下,问题确保你明白你为它付出的代价。
如果你决定去与基准的主要原因使用const的引用,使有可能有隐性上溯造型时,你需要通过共享指针从类继承您在接口使用类对象。
除了什么litb说,我想指出的是,这可能是由常量引用在第二个例子中通过,这样你确信你不小心修改。
struct A {
shared_ptr<Message> msg;
shared_ptr<Message> * ptr_msg;
}
按值传递:
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;
通过引用传递:
void set(shared_ptr<Message>& msg) { this->msg = msg; /// reference count will be added, because reference is just an alias. }
通过指针传递:
void set(shared_ptr<Message>* msg) { this->ptr_msg = msg; /// reference count will not be added; }
每个码片必须携带一些感。如果通过值处处应用程序通过一个共享的指针,这种手段的“我不确定什么别的地方怎么回事,所以我赞成原安全的”。这不是我所说的好信心标志,以其他程序员谁可以参考代码。
总之,即使功能得到一个const引用和你是“不确定”,你仍然可以在该函数的开始创建共享指针的副本,添加一个强有力的参考指针。这也可以看作是一个关于设计的暗示(“指针可以别处修饰”)。
所以,是IMO,默认应该是 “按const引用传递强>”。