传递 const shared_ptr<T>& 而不是仅 shared_ptr<T> 作为参数
Passing const shared_ptr<T>& versus just shared_ptr<T> as parameter
我已经阅读了很多关于应用程序中涉及智能指针时的性能问题的讨论。常见的建议之一是将智能指针作为 const& 而不是副本传递,如下所示:
void doSomething(std::shared_ptr<T> o) {}
对比
void doSomething(const std::shared_ptr<T> &o) {}
但是,第二种变体实际上是否违背了共享指针的目的?我们实际上在这里共享共享指针,所以如果由于某些原因指针在调用代码中被释放(考虑重入或副作用),那么 const 指针将变得无效。共享指针实际上应该防止的情况。我知道 const& 节省了一些时间,因为不涉及复制,也没有锁定来管理引用计数。但代价是代码的安全性降低了,对吧?
如果目标函数是同步的并且只在执行期间使用参数,并且在 return 上不再需要它,我认为通过 const &
是完全合理的。在这里节省增加引用计数的成本是合理的——因为在这些有限的情况下你真的不需要额外的安全——前提是你理解其中的含义并确定代码是安全的。
这与函数需要保存参数(例如在 class 成员中)供以后重新引用时相反。
将 shared_ptr
传递给 const&
的优点是引用计数不必先增加再减少。因为这些操作必须是线程安全的,所以它们可能很昂贵。
你说的很对,你可能有一个通过引用传递的链,稍后会使链的头部无效。在一个具有现实世界后果的现实世界项目中,这曾经发生在我身上。一个函数在一个容器中找到了一个 shared_ptr
并将对它的引用传递到调用堆栈中。调用堆栈深处的一个函数从容器中删除了对象,导致所有引用突然指向一个不再存在的对象。
因此,当您通过引用传递某些内容时,调用者必须确保它在函数调用的生命周期内存在。如果这是一个问题,请不要使用引用传递。
(我假设你有一个用例,其中有一些特定的原因要通过 shared_ptr
而不是通过引用。最常见的原因是调用的函数可能需要延长生命对象的。)
更新:感兴趣的人可以了解有关该错误的更多详细信息:该程序具有共享的对象并实现了内部线程安全。它们被保存在容器中,函数延长它们的生命周期是很常见的。
这种特殊类型的对象可以存在于两个容器中。一个在它处于活动状态时,一个在它不活动时。一些操作对活动对象起作用,一些操作对非活动对象起作用。当在非活动对象上接收到命令使其处于活动状态时发生错误情况,而该对象的唯一 shared_ptr
由非活动对象的容器持有。
非活动对象位于其容器中。容器中对 shared_ptr
的引用已通过引用传递给命令处理程序。通过引用链,这个 shared_ptr
最终到达了代码,该代码意识到这是一个必须激活的非活动对象。该对象已从非活动容器中删除(它破坏了非活动容器的 shared_ptr
)并添加到活动容器中(它添加了对传递给 "add" 例程的 shared_ptr
的另一个引用)。
此时,存在的对象的唯一 shared_ptr
可能是非活动容器中的对象。调用堆栈中的每个其他函数都只是引用它。当对象从非活动容器中移除时,该对象可能会被销毁,所有这些引用都指向一个不再存在的 shared_ptr
。
花了大约一个月的时间来解决这个问题。
首先,两者之间存在语义差异:
按值传递共享指针表明您的函数将获得其底层对象所有权的一部分。
除了强制此函数的用户使用 shared_ptr 之外,将 shared_ptr 作为 const 引用传递并不表示仅通过 const 引用(或原始指针)传递基础对象的任何意图。所以大部分都是垃圾。
只要它们在语义上不同,比较它们的性能影响是无关紧要的。
来自 https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/
Don’t pass a smart pointer as a function parameter unless you want to
use or manipulate the smart pointer itself, such as to share or
transfer ownership.
这次我完全同意 Herb :)
还有另一段引述,更直接地回答了问题
Guideline: Use a non-const shared_ptr& parameter only to modify the shared_ptr. Use a const shared_ptr& as a parameter only if you’re not sure whether or not you’ll take a copy and share ownership; otherwise use * instead (or if not nullable, a &)
如果您的方法不涉及所有权修改,则您的方法通过复制或常量引用获取 shared_ptr 没有任何好处,它会污染 API 并可能产生开销(如果通过副本)
干净的方法是根据您的用例通过 const ref 或 ref 传递基础类型
void doSomething(const T& o) {}
auto s = std::make_shared<T>(...);
// ...
doSomething(*s);
底层指针在方法调用过程中无法释放
首先,不要将 shared_ptr
传递到调用链中,除非其中一个被调用函数有可能存储它的副本。传递对被引用对象的引用,或指向该对象的原始指针,或者可能是一个框,具体取决于它是否可选。
但是当你确实传递 shared_ptr
时,最好通过引用 const
来传递它,因为复制 shared_ptr
会产生额外的开销。复制必须更新共享引用计数,并且此更新必须是线程安全的。因此,可以(安全地)避免一些低效率问题。
关于
” the price is making the code less safe, right?
没有。价格是天真生成的机器代码中的额外间接,但编译器会管理它。所以这一切都是为了避免编译器不能为你优化的小但完全不必要的开销,除非它非常聪明。
正如 David Schwarz 在他的回答中举例说明的那样,当您通过引用 const
传递 别名问题 时,您调用的函数又会更改或调用一个函数改变原始对象,是可能的。根据墨菲定律,它会在最不方便的时间、以最大的成本和最复杂、难以理解的代码发生。但这与参数是 string
还是 shared_ptr
或其他什么无关。幸运的是,这是一个非常罕见的问题。但请记住,也 用于传递 shared_ptr
个实例。
正如 C++ - shared_ptr: horrible speed 中指出的那样,复制 shared_ptr
需要时间。构造涉及原子增量和破坏原子减量,原子更新(无论是增量还是减量)可能会阻止许多编译器优化(内存 loads/stores 无法跨操作迁移)并且在硬件级别涉及 CPU 缓存一致性协议,以确保整个缓存行由进行修改的核心拥有(独占模式)。
所以,你是对的,std::shared_ptr<T> const&
可以用作 std::shared_ptr<T>
的性能改进。
你也说得对,pointer/reference 理论上存在因某些别名而变得悬空的风险。
也就是说,任何 C++ 程序中都已经存在风险:任何单独使用指针或引用都是一种风险。我认为 std::shared_ptr<T> const&
的几次出现与 T&
、T const&
、T*
、...[ 的总使用次数相比应该是九牛一毛。 =36=]
最后,我想指出传递 shared_ptr<T> const&
是 奇怪的 。以下情况很常见:
shared_ptr<T>
:我需要 shared_ptr
的副本
T*
/T const&
/T&
/T const&
:我需要 T
的句柄(可能为空)
下一个案例不太常见:
shared_ptr<T>&
:我可以重新安装 shared_ptr
但通过shared_ptr<T> const&
?合法用途非常非常罕见。
传递 shared_ptr<T> const&
你想要的只是对 T
的引用是一种反模式:你强迫用户使用 shared_ptr
而他们可以分配 T
其它的办法!大多数时候 (99,99..%),你不应该关心 T
是如何分配的。
你会传递 shared_ptr<T> const&
的唯一情况是你不确定是否需要副本,并且因为你已经分析了程序并表明这个原子 increment/decrement是一个瓶颈,您已决定将副本的创建推迟到仅在需要它的情况下。
这是一种极端情况,因此对 shared_ptr<T> const&
的任何使用都应高度怀疑。
我已经阅读了很多关于应用程序中涉及智能指针时的性能问题的讨论。常见的建议之一是将智能指针作为 const& 而不是副本传递,如下所示:
void doSomething(std::shared_ptr<T> o) {}
对比
void doSomething(const std::shared_ptr<T> &o) {}
但是,第二种变体实际上是否违背了共享指针的目的?我们实际上在这里共享共享指针,所以如果由于某些原因指针在调用代码中被释放(考虑重入或副作用),那么 const 指针将变得无效。共享指针实际上应该防止的情况。我知道 const& 节省了一些时间,因为不涉及复制,也没有锁定来管理引用计数。但代价是代码的安全性降低了,对吧?
如果目标函数是同步的并且只在执行期间使用参数,并且在 return 上不再需要它,我认为通过 const &
是完全合理的。在这里节省增加引用计数的成本是合理的——因为在这些有限的情况下你真的不需要额外的安全——前提是你理解其中的含义并确定代码是安全的。
这与函数需要保存参数(例如在 class 成员中)供以后重新引用时相反。
将 shared_ptr
传递给 const&
的优点是引用计数不必先增加再减少。因为这些操作必须是线程安全的,所以它们可能很昂贵。
你说的很对,你可能有一个通过引用传递的链,稍后会使链的头部无效。在一个具有现实世界后果的现实世界项目中,这曾经发生在我身上。一个函数在一个容器中找到了一个 shared_ptr
并将对它的引用传递到调用堆栈中。调用堆栈深处的一个函数从容器中删除了对象,导致所有引用突然指向一个不再存在的对象。
因此,当您通过引用传递某些内容时,调用者必须确保它在函数调用的生命周期内存在。如果这是一个问题,请不要使用引用传递。
(我假设你有一个用例,其中有一些特定的原因要通过 shared_ptr
而不是通过引用。最常见的原因是调用的函数可能需要延长生命对象的。)
更新:感兴趣的人可以了解有关该错误的更多详细信息:该程序具有共享的对象并实现了内部线程安全。它们被保存在容器中,函数延长它们的生命周期是很常见的。
这种特殊类型的对象可以存在于两个容器中。一个在它处于活动状态时,一个在它不活动时。一些操作对活动对象起作用,一些操作对非活动对象起作用。当在非活动对象上接收到命令使其处于活动状态时发生错误情况,而该对象的唯一 shared_ptr
由非活动对象的容器持有。
非活动对象位于其容器中。容器中对 shared_ptr
的引用已通过引用传递给命令处理程序。通过引用链,这个 shared_ptr
最终到达了代码,该代码意识到这是一个必须激活的非活动对象。该对象已从非活动容器中删除(它破坏了非活动容器的 shared_ptr
)并添加到活动容器中(它添加了对传递给 "add" 例程的 shared_ptr
的另一个引用)。
此时,存在的对象的唯一 shared_ptr
可能是非活动容器中的对象。调用堆栈中的每个其他函数都只是引用它。当对象从非活动容器中移除时,该对象可能会被销毁,所有这些引用都指向一个不再存在的 shared_ptr
。
花了大约一个月的时间来解决这个问题。
首先,两者之间存在语义差异: 按值传递共享指针表明您的函数将获得其底层对象所有权的一部分。 除了强制此函数的用户使用 shared_ptr 之外,将 shared_ptr 作为 const 引用传递并不表示仅通过 const 引用(或原始指针)传递基础对象的任何意图。所以大部分都是垃圾。
只要它们在语义上不同,比较它们的性能影响是无关紧要的。
来自 https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/
Don’t pass a smart pointer as a function parameter unless you want to use or manipulate the smart pointer itself, such as to share or transfer ownership.
这次我完全同意 Herb :)
还有另一段引述,更直接地回答了问题
Guideline: Use a non-const shared_ptr& parameter only to modify the shared_ptr. Use a const shared_ptr& as a parameter only if you’re not sure whether or not you’ll take a copy and share ownership; otherwise use * instead (or if not nullable, a &)
如果您的方法不涉及所有权修改,则您的方法通过复制或常量引用获取 shared_ptr 没有任何好处,它会污染 API 并可能产生开销(如果通过副本)
干净的方法是根据您的用例通过 const ref 或 ref 传递基础类型
void doSomething(const T& o) {}
auto s = std::make_shared<T>(...);
// ...
doSomething(*s);
底层指针在方法调用过程中无法释放
首先,不要将 shared_ptr
传递到调用链中,除非其中一个被调用函数有可能存储它的副本。传递对被引用对象的引用,或指向该对象的原始指针,或者可能是一个框,具体取决于它是否可选。
但是当你确实传递 shared_ptr
时,最好通过引用 const
来传递它,因为复制 shared_ptr
会产生额外的开销。复制必须更新共享引用计数,并且此更新必须是线程安全的。因此,可以(安全地)避免一些低效率问题。
关于
” the price is making the code less safe, right?
没有。价格是天真生成的机器代码中的额外间接,但编译器会管理它。所以这一切都是为了避免编译器不能为你优化的小但完全不必要的开销,除非它非常聪明。
正如 David Schwarz 在他的回答中举例说明的那样,当您通过引用 const
传递 别名问题 时,您调用的函数又会更改或调用一个函数改变原始对象,是可能的。根据墨菲定律,它会在最不方便的时间、以最大的成本和最复杂、难以理解的代码发生。但这与参数是 string
还是 shared_ptr
或其他什么无关。幸运的是,这是一个非常罕见的问题。但请记住,也 用于传递 shared_ptr
个实例。
正如 C++ - shared_ptr: horrible speed 中指出的那样,复制 shared_ptr
需要时间。构造涉及原子增量和破坏原子减量,原子更新(无论是增量还是减量)可能会阻止许多编译器优化(内存 loads/stores 无法跨操作迁移)并且在硬件级别涉及 CPU 缓存一致性协议,以确保整个缓存行由进行修改的核心拥有(独占模式)。
所以,你是对的,std::shared_ptr<T> const&
可以用作 std::shared_ptr<T>
的性能改进。
你也说得对,pointer/reference 理论上存在因某些别名而变得悬空的风险。
也就是说,任何 C++ 程序中都已经存在风险:任何单独使用指针或引用都是一种风险。我认为 std::shared_ptr<T> const&
的几次出现与 T&
、T const&
、T*
、...[ 的总使用次数相比应该是九牛一毛。 =36=]
最后,我想指出传递 shared_ptr<T> const&
是 奇怪的 。以下情况很常见:
shared_ptr<T>
:我需要shared_ptr
的副本
T*
/T const&
/T&
/T const&
:我需要T
的句柄(可能为空)
下一个案例不太常见:
shared_ptr<T>&
:我可以重新安装shared_ptr
但通过shared_ptr<T> const&
?合法用途非常非常罕见。
传递 shared_ptr<T> const&
你想要的只是对 T
的引用是一种反模式:你强迫用户使用 shared_ptr
而他们可以分配 T
其它的办法!大多数时候 (99,99..%),你不应该关心 T
是如何分配的。
你会传递 shared_ptr<T> const&
的唯一情况是你不确定是否需要副本,并且因为你已经分析了程序并表明这个原子 increment/decrement是一个瓶颈,您已决定将副本的创建推迟到仅在需要它的情况下。
这是一种极端情况,因此对 shared_ptr<T> const&
的任何使用都应高度怀疑。