将const shared_ptr<T>&作为参数传递与仅传递shared_ptr<T>有何区别?

34

最近我阅读了很多关于智能指针在应用程序中存在性能问题的讨论。其中一个经常建议的解决方案是将智能指针作为const&而不是作为副本进行传递,像这样:

void doSomething(std::shared_ptr<T> o) {}

对决

void doSomething(const std::shared_ptr<T> &o) {}

然而,第二个变体实际上不是共享指针的目的吗? 在这里,我们实际上正在共享共享指针,因此,如果由于某些原因在调用代码中释放指针(考虑可重入性或副作用),那么该const指针将变为无效。这是共享指针实际上应该防止的情况。 我明白const&可以节省时间,因为没有涉及复制并且没有锁定来管理引用计数。 但代价是使代码不太安全,对吧?


12
在管理对象的生命周期时,你应该只使用智能指针。调用函数时传递 原始指针 或者 引用 给由 智能指针 管理的对象。参见:https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rr-smartptrparam - Galik
8
这要看情况,你是需要在 std::shared_ptr 上执行操作还是只需要对指向的对象进行操作?如果是后者,那么就使用 const T&doSomething 函数不需要关心对象是如何存储的。 - TartanLlama
1
如果可能的话,我同意以上所有观点,尽量只使用const T&。 - Julien Lopez
1
@DavidSchwartz 这完全属于“管理对象生命周期”的范畴,不是吗? - Quentin
@Quentin 对的。因此,除非您在限定语中加上“除非这些函数可能需要扩展对象的生命周期”,否则不能说“在调用函数时,传递原始指针或智能指针管理的对象的引用”。最有可能的情况是,您拥有shared_ptr的原因是您有一个使用案例,您希望需要扩展生命周期。 - David Schwartz
显示剩余4条评论
6个回答

24
通过使用 const& 传递 shared_ptr 的好处在于不需要增加和减少引用计数。由于这些操作必须是线程安全的,因此可能会很昂贵。
你说得很对,确实存在一个风险,即可能存在一系列通过引用传递的链,后来使链的头部失效。我曾经在一个真实项目中遇到过这种情况,并产生了真实的后果。一个函数在容器中找到了一个 shared_ptr,然后将其引用传递到调用栈中。调用栈深处的一个函数从容器中删除了对象,导致所有引用突然指向一个不存在的对象。
因此,当你通过引用传递东西时,调用者必须确保它在函数调用的生命周期内存活。如果这是一个问题,请不要使用引用传递。
(我假设您有一个使用 shared_ptr 而不是引用传递的特定原因的用例。最常见的原因是被调用的函数可能需要延长对象的生命周期。)
更新:对于那些感兴趣的人,以下是关于这个 bug 的更多细节:该程序具有共享对象并实现内部线程安全的功能。它们保存在容器中,函数通常会延长它们的生命周期。
这种特定类型的对象可以存在于两个容器中。一个用于活动对象,另一个用于非活动对象。一些操作在活动对象上工作,一些在非活动对象上工作。错误情况发生在对非活动对象收到命令时,该命令使其变为活动状态,而唯一的 shared_ptr 对象由非活动对象的容器持有。

该非活动对象位于其容器中。将指向容器中的shared_ptr的引用通过引用传递给命令处理程序。通过一系列引用,这个shared_ptr最终到达了意识到这是一个需要激活的非活动对象的代码。该对象被从非活动容器中删除(这会销毁非活动容器的shared_ptr),并添加到活动容器中(这会向“添加”例程传递的shared_ptr添加另一个引用)。

此时,唯一存在的指向对象的shared_ptr可能是在非活动容器中的那个。调用栈中的每个其他函数只是对它的引用。当对象从非活动容器中删除时,对象可以被销毁,所有这些引用都指向一个不再存在的shared_ptr

解决这个问题大约花了一个月的时间。


2
这正是我担心使用 shared_ptr 共享时会出现的情况。 - Mike Lischke
在将对象从非活动对象中移除之前,您是否可以先将其添加到活动对象中? - gnasher729
我记得在我们特定的情况下这是不可能的,因为那会需要一个中间点,在那个点上对象处于非法状态且没有保持任何锁。但那是我们特殊情况的怪癖。否则,“先添加再移除”就可以解决问题。 - David Schwartz
1
调用它的代码并不真正知道它可以删除它所引用的对象,这就是失败的关键点。如果您从容器中获得了一个shared_ptr,则知道容器可能会被修改,但不知道将进行什么修改,那么您不能传递对该shared_ptr的引用。实际上,如果我们在上一句话中用int替换shared_ptr,则同样适用,并且如果有人问“我注意到通过引用传递int比值更快,我应该盲目地替换所有调用吗?” 不行。 - Steve Jessop
1
所以我认为这个答案是正确的,警告不要仅仅因为对象很难复制就将所有按值传递改为按引用传递。事实上,原因并不特定于“shared_ptr”,但这并不妨碍它成为不这样做的关键原因 :-) 无论多么昂贵,您都不能仅仅传递对生命周期未得到保证的东西的引用。您的容器示例可能出错是因为有人假设“啊,这里涉及到一个shared_ptr,生命周期会很好,因为shared_ptr正在发挥作用”,但实际上并非如此。 - Steve Jessop

12
首先,除非被调用的函数中有一个会存储其副本,否则不要将shared_ptr下传到调用链。传递对所引用对象的引用、该对象的原始指针或可能是可选项的盒子。

但是当您传递shared_ptr时,最好通过const 引用传递,因为复制shared_ptr会产生额外的开销。复制必须更新共享引用计数,并且此更新必须是线程安全的。因此,存在可以(安全地)避免的小小的低效率。

关于

价格使代码更不安全,对吗?

不对。价格是简单生成的机器代码中的额外间接性,但编译器管理它。因此,只需避免一个微小但完全不必要的开销,编译器就不能为您优化,除非它非常聪明。

正如David Schwarz在他的回答中所示,当您通过引用传递const时,可能会出现别名问题,即您调用的函数依次更改或调用更改原始对象的函数。按照墨菲定律,在最不方便的时间、最大的成本和最离奇的、难以理解的代码中会发生这种情况。但是,这与参数是string还是shared_ptr或其他内容无关。令人欣慰的是,这是非常罕见的问题。但是请记住,同样适用于传递shared_ptr实例。


你指出引用变化的问题不是shared_ptr特有的问题,这一点非常重要!我认为,在所有答案中,它最好地指出了实际问题。 - Cort Ammon

7
首先,这两者之间存在语义上的区别: 通过值传递共享指针表示您的函数将拥有底层对象的一部分所有权。 将shared_ptr作为const引用传递除了强制使用此函数的用户使用shared_ptr之外,并没有表明任何意图仅通过const引用(或原始指针)传递底层对象。所以大多数是无用的。
比较这些的性能影响是不相关的,只要它们在语义上不同。
来自https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ 引用块: 不要将智能指针作为函数参数传递,除非您想要使用或操作智能指针本身,例如共享或转移所有权。
我完全同意Herb的观点:)
还有另一个来自相同文章的引用,更直接地回答了问题。
指南:仅在需要修改 shared_ptr 时使用非 const 的 shared_ptr& 参数。如果您不确定是否会复制并共享所有权,请仅使用 const shared_ptr& 作为参数;否则使用 *(或者如果不可空,则使用 &)。

1
没错,但这与问题并不相关。 - David Schwartz
我认为这非常相关,但我从同一处添加了更明确的引用。请注意,对我来说,更明确的引用暗示了一个有问题的设计,因此我倾向于说const shared_ptr <>&在函数参数中大多数情况下是无用的。 - Slava
1
й—®йўҳжҳҜе…ідәҺйҖҡиҝҮеҖјдј йҖ’shared_ptrдёҺйҖҡиҝҮconstеј•з”Ёдј йҖ’е®ғд№Ӣй—ҙзҡ„еҢәеҲ«гҖӮдҪ зҡ„еӣһзӯ”дёӯжІЎжңүж¶үеҸҠеҲ°иҝҷдёҖзӮ№гҖӮ - David Schwartz

6
正如在C++ - shared_ptr: horrible speed中所指出的那样,复制一个shared_ptr需要时间。构造涉及原子递增,而销毁则涉及原子递减,原子更新(无论是递增还是递减)可能会阻止许多编译器优化(内存加载/存储无法迁移操作),并且在硬件级别上涉及CPU缓存一致性协议,以确保整个缓存行由执行修改的核心拥有(独占模式)。
因此,您是正确的,std::shared_ptr<T> const&可以用作比仅使用std::shared_ptr<T>更好的性能提升。
您也是正确的,由于某些别名,指针/引用存在成为悬空指针/引用的理论风险。
话虽如此,这个风险在任何C++程序中都是潜在的:任何单个指针或引用的使用都是风险。我认为,std::shared_ptr<T> const&的少数情况应该与T&T const&T*等的总使用次数相比较微不足道。
最后,我想指出传递shared_ptr<T> const&是“奇怪的”。以下情况很常见:
  • shared_ptr<T>:我需要一个shared_ptr的副本
  • T*/T const&/T&/T const&:我需要一个(可能为null的)指向T的句柄
下一个情况要少得多:
  • shared_ptr<T>&:我可能会重新设置shared_ptr
但传递shared_ptr<T> const&?合法用途非常非常罕见。
当您只想要引用T时,传递shared_ptr<T> const&是一种反模式:您强制用户使用shared_ptr,而他们可以以其他方式分配T!大多数情况下(99.99..%),您不应该关心如何分配T
唯一的情况是,如果您不确定是否需要副本,并且因为您已经对程序进行了分析并显示这个原子递增/递减是瓶颈,您已经决定将副本的创建推迟到仅在需要时才创建。
这是一个非常特殊的情况,任何使用shared_ptr<T> const&的地方都应该极度谨慎。

你对那个问题的引用很好地回答了我的隐含问题,这个问题到目前为止还没有被涉及到,非常感谢。 - Mike Lischke
@MikeLischke:这正是我包含它的原因 ;) - Matthieu M.
没有什么奇怪的,有效的C++程序中不存在null引用。不过提供更多信息的链接很好。所以,+和-,= 没有投票权。 :) - Cheers and hth. - Alf
@Cheersandhth.-Alf:空引用指的是指针(可能为空),它们不是标准中所谓的“引用”...我能理解这很令人困惑 :x 我已经改成“句柄”了,这样更好吗? - Matthieu M.

4

如果您的方法没有涉及所有权的修改,则不需要通过复制或const引用方式使用shared_ptr,这会污染API并可能增加开销(如果通过复制传递)

干净的方法是根据您的用例通过const引用或引用方式传递基础类型

void doSomething(const T& o) {}

auto s = std::make_shared<T>(...);
// ...
doSomething(*s);

在方法调用期间无法释放底层指针。


1
иҝҷ并没жңүеӣһзӯ”жүҖй—®зҡ„й—®йўҳгҖӮй—®йўҳжҳҜе…ідәҺйҖҡиҝҮеҖјдј йҖ’shared_ptrдёҺйҖҡиҝҮconstеј•з”Ёдј йҖ’shared_ptrд№Ӣй—ҙзҡ„еҢәеҲ«гҖӮ - David Schwartz
问题在于,除非你想污染API,否则传递共享指针的const &毫无用处,而通过复制传递shared_ptr可能是有用的,如果在方法的作用域中,您将创建一些包装器,这些包装器将超出原始shared_ptr的生命周期。 - Stephane Molina

3
我认为如果目标函数是同步的,并且仅在执行期间使用参数并且在返回后不再需要它,则通过const &传递参数是完全合理的。在这种情况下,可以节省增加引用计数的成本-因为在这些有限情况下您实际上不需要额外的安全性-前提是您了解其影响并确信代码是安全的。
这与函数需要保存参数(例如在类成员中)以供以后重新引用时相对。

这与函数需要保存参数以供稍后重新引用的情况相反,这是毫无意义的。没有任何限制阻止您将通过引用传递的“shared_ptr”复制到“const”中。 - Cheers and hth. - Alf
1
确实,没有任何东西禁止您复制const &。然而,我认为,如果这是您的意图,让函数按值接受参数会使事情更清晰。这种清晰度也不会以更慢的操作为代价:因为您始终可以将shared_ptr按值参数移动到其在函数内的最终位置,如果需要,因此您不需要额外的副本。 - Smeeheey
是的,当函数不再传递shared_ptr时。实际上,按值移动传递是Herb推荐的方式,所以如果这就是你的意思,那么我之前说它毫无意义是错误的。但是当你有一个调用链,即使只有两个级别,成本也会反过来(复制+复制+移动与ref+ref+copy+move),如果必须选择单一约定,则支持通过引用传递到const - Cheers and hth. - Alf
即使在一个链中,如果代码以某种方式组织(即在任何给定级别上链接调用是shared_ptr的最后使用),那么您可以沿着链移动共享指针,这样就避免了您提到的额外复制。话虽如此,我相信仍然有一些情况适合您的方法。我只是不同意最初的观点,即我的原始评论是“无意义的”。 - Smeeheey
通常情况下,我们不能简单地将对象在调用链中“移动”,因为调用通常不是块中对象最后使用的地方。因此,这并不适用于一般约定。这只是一个非常特殊的情况。 - Cheers and hth. - Alf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接