提问人:abigagli 提问时间:11/29/2008 最后编辑:Nullabigagli 更新时间:6/25/2020 访问量:53815
C++ - 传递对 std::shared_ptr 或 boost::shared_ptr 的引用
C++ - passing references to std::shared_ptr or boost::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();
...
}
在这两种情况下,我都看不出传递 by 值而不是 by 引用的充分理由。按值传递只会“暂时”增加由于复制而导致的引用计数,然后在退出函数范围时递减引用计数。
我是否忽略了什么?boost::shared_ptr<foo>
澄清一下,在阅读了几个答案之后:我完全同意过早优化的问题,我总是尝试先分析热点,然后再工作。如果你明白我的意思,我的问题更多是从纯粹的技术代码角度来看的。
答:
是的,在那里做参考是可以的。您不打算为该方法提供共享所有权;它只想和它一起工作。您也可以参考第一种情况,因为无论如何您都会复制它。但对于第一种情况,它需要所有权。有这个技巧仍然只复制一次:
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);
评论
我建议不要这样做,除非你和与你一起工作的其他程序员真的知道你们都在做什么。
首先,你不知道你的类的接口会如何发展,你想防止其他程序员做坏事。通过引用传递shared_ptr不是程序员应该期望看到的,因为它不是惯用的,这使得它很容易被错误地使用。防御性编程:使界面难以错误使用。通过引用只会在以后引发问题。
其次,在你知道这个特定的类会成为一个问题之前,不要优化。首先是配置文件,然后如果你的程序真的需要通过引用传递来提供的提升,那么也许吧。否则,不要为小事(即按值传递所需的额外 N 条指令)而烦恼,而是担心设计、数据结构、算法和长期可维护性。
评论
除了 litb 所说的之外,我还想指出,在第二个示例中,它可能是通过 const 引用传递的,这样您就可以确保不会意外修改它。
我会避免“普通”引用,除非函数显式可以修改指针。
在调用小函数时,A 可能是一个明智的微优化 - 例如,启用进一步的优化,例如内联某些条件。此外,递增/递减 - 因为它是线程安全的 - 是一个同步点。不过,我不认为这在大多数情况下会产生很大的不同。const &
通常,除非您有理由不这样做,否则您应该使用更简单的样式。然后,要么一致地使用它,要么添加一个注释,说明为什么如果你只在几个地方使用它。const &
不同实例的要点是(尽可能)保证只要它在作用域内,它指向的对象就仍然存在,因为它的引用计数至少为 1。shared_ptr
shared_ptr
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()
这完全取决于那些“......”代码的各个部分。如果你在第一个“...”中调用某物怎么办?这有将 A 清除到同一对象的副作用(在代码的另一部分的某个地方)?如果它恰好是该物体唯一剩下的不同呢?再见对象,就在你即将尝试使用它的地方。shared_ptr
shared_ptr
所以有两种方法可以回答这个问题:
非常仔细地检查整个程序的源代码,直到您确定对象不会在函数体期间死亡。
将参数改回不同的对象,而不是引用。
这里适用的一般建议是:不要为了性能而费心对你的代码进行冒险的更改,直到你在分析器中将你的产品定时到一个实际的情况,并最终衡量出你想要做出的改变将对性能产生重大影响。
评论者 JQ 的更新
这是一个人为的例子。这是故意简单的,所以错误会很明显。在真实的例子中,错误并不那么明显,因为它隐藏在真实细节的层中。
我们有一个函数可以在某处发送消息。它可能是一个大消息,因此我们不使用在传递到多个位置时可能会被复制的 a,而是使用 a to a 字符串: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!
现在来了 Mr 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;
但当然,它仍然会继续并崩溃,因为在调用时永远不会为 null。msg
send_message
正如我所说,在一个简单的例子中,所有的代码都如此接近,很容易找到错误。但是在实际程序中,由于相互包含指针的可变对象之间的关系更复杂,因此很容易犯错误,并且很难构建必要的测试用例来检测错误。
一个简单的解决方案是,你希望一个函数能够依赖一个持续的非 null,是让函数分配它自己的 true ,而不是依赖于对现有 .shared_ptr
shared_ptr
shared_ptr
缺点是复制 a 不是免费的:即使是“无锁”实现也必须使用互锁操作来遵守线程保证。因此,在某些情况下,通过将 a 更改为 .但这不是可以安全地对所有程序进行的更改。它改变了程序的逻辑含义。shared_ptr
shared_ptr
shared_ptr &
请注意,如果我们使用 throughout 而不是 ,而不是:std::string
std::shared_ptr<std::string>
previous_message = 0;
为了清除消息,我们说:
previous_message.clear();
那么症状将是意外发送空消息,而不是未定义的行为。一个非常大的字符串的额外副本的成本可能比复制一个 的成本要大得多,因此权衡可能不同。shared_ptr
评论
通过 s 是明智的。它不太可能引起麻烦(除非在函数调用期间删除引用的极少数情况,如 Earwicker 所详述的那样),如果您传递大量这些内容,它可能会更快。记得;默认值为线程安全,因此复制它包括线程安全增量。shared_ptr
const&
shared_ptr
boost::shared_ptr
尝试使用 而不是仅仅使用 ,因为临时对象可能不会通过非常量引用传递。(即使 MSVC 中的语言扩展仍然允许您执行此操作)const&
&
评论
我假设您熟悉过早的优化,并且出于学术目的或因为您已经隔离了一些性能不佳的预先存在的代码而提出这个问题。
通过引用传递是可以的
通过常量引用传递更好,并且通常可以使用,因为它不会在指向的对象上强制常量。
您不会因使用引用而面临丢失指针的风险。该引用证明您在堆栈的前面有一个智能指针的副本,并且只有一个线程拥有调用堆栈,因此预先存在的副本不会消失。
由于您提到的原因,使用引用通常更有效,但不能保证。请记住,取消引用对象也需要工作。理想的参考使用场景是,如果您的编码风格涉及许多小函数,则指针在使用之前会从一个函数传递到另一个函数。
应始终避免将智能指针存储为参考。您的示例显示了正确的用法。Class::take_copy_of_sp(&sp)
评论
假设我们不关心常量正确性(或者更多,您的意思是允许函数能够修改或共享传入的数据的所有权),按值传递 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 旨在提高代码安全性。如果某些东西需要这种利基优化,那么从副本到参考会容易得多。
我主张通过常量引用传递共享指针 - 一种语义,即使用指针传递的函数不拥有指针,这对开发人员来说是一个干净的习惯。
唯一的缺陷是在多线程程序中,共享指针指向的对象在另一个线程中被销毁。因此,可以肯定地说,在单线程程序中使用共享指针的 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与否,无论是否常量,如果您有设计缺陷,例如直接或间接共享线程之间的所有权,错过使用对象,则可能会发生这种情况,您有循环所有权或其他所有权错误。sp
delete this
桑迪写道:“似乎这里的所有优点和缺点实际上都可以推广到任何通过引用传递的类型,而不仅仅是shared_ptr。
在某种程度上是正确的,但使用 shared_ptr 的目的是消除对对象生存期的担忧,并让编译器为您处理。如果要通过引用传递共享指针,并允许引用计数对象的客户端调用可能释放对象数据的非常量方法,则使用共享指针几乎毫无意义。
我在上一句话中写了“几乎”,因为性能可能是一个问题,在极少数情况下它“可能”是合理的,但我自己也会避免这种情况,并自己寻找所有可能的其他优化解决方案,例如认真考虑添加另一个级别的间接、惰性评估等。
存在于作者之后的代码,甚至发布作者的记忆,需要对行为(特别是关于对象生存期的行为)的隐式假设,需要清晰、简洁、可读的文档,然后许多客户端无论如何都不会阅读它!简单几乎总是胜过效率,而且几乎总是有其他方法可以提高效率。如果您确实需要通过引用传递值,以避免 reference-counted-objects(和 equals 运算符)的复制构造函数进行深度复制,那么也许您应该考虑如何使深度复制的数据成为可以快速复制的引用计数指针。(当然,这只是一个可能不适用于您的情况的设计方案)。
我发现自己不同意得票最高的答案,所以我去寻找专家意见,他们就在这里。 从 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,“除非你想使用或操纵智能指针本身,例如共享或转让所有权。
评论
shared_ptr
const &
我曾经在一个项目中工作过,这个项目的原则非常强烈,即按值传递智能指针。当我被要求做一些性能分析时,我发现对于智能指针的引用计数器的增加和减少,应用程序花费了 4-6% 的已用处理器时间。
如果您想按值传递智能指针,只是为了避免在奇怪的情况下出现问题,正如 Daniel Earwicker 所描述的那样,请确保您了解为此付出的代价。
如果您决定使用引用,那么使用 const 引用的主要原因是,当您需要将共享指针从继承您在接口中使用的类的类传递到对象时,可以进行隐式转换。
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; }
我还没有看到提到的一件事是,当您通过引用传递共享指针时,如果要通过对基类共享指针的引用传递派生类共享指针,则会丢失所获得的隐式转换。
例如,此代码将产生错误,但如果您进行更改,以便不通过引用传递共享指针,则此代码将起作用。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;
}
每个代码段都必须有一定的意义。如果在应用程序中的所有位置按值传递共享指针,则意味着“我不确定其他地方发生了什么,因此我赞成原始安全性”。对于其他可以查阅代码的程序员来说,这不是一个良好的信心信号。
无论如何,即使函数获得了常量引用,并且您“不确定”,您仍然可以在函数的头部创建共享指针的副本,以添加对指针的强引用。这也可以看作是对设计的暗示(“指针可以在其他地方修改”)。
所以是的,IMO,默认值应该是“通过常量引用传递”。
评论