【问题标题】:Passing const shared_ptr<T>& versus just shared_ptr<T> as parameter传递 const shared_ptr<T>& 而不是 shared_ptr<T> 作为参数
【发布时间】:2016-06-03 09:17:33
【问题描述】:

我已经阅读了大量关于应用程序中涉及智能指针时的性能问题的讨论。常见的建议之一是将智能指针作为 const& 而不是副本传递,如下所示:

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

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

但是,第二个变体实际上不是破坏了共享指针的目的吗?我们实际上在这里共享共享指针,所以如果由于某些原因指针在调用代码中被释放(考虑可重入性或副作用),则 const 指针将变为无效。共享指针实际上应该防止的情况。我知道 const& 节省了一些时间,因为不涉及复制,也没有锁定来管理引用计数。但代价是代码的安全性降低了,对吧?

【问题讨论】:

  • 您应该只使用智能指针来管理对象的生命周期。当调用函数时,将 原始指针引用 传递给 智能指针 正在管理的对象。见:github.com/isocpp/CppCoreGuidelines/blob/master/…
  • 这取决于,您真的需要对std::shared_ptr 或仅指针进行操作吗?如果是后者,就拿一个const T&amp;doSomething 不需要关心对象是如何存储的。
  • 我同意以上所有观点,如果可能的话,请仅使用 const T&。
  • @Galik 如果函数需要延长对象的寿命怎么办?
  • @DavidSchwartz 完全属于“管理对象的生命周期”类别,不是吗?

标签: c++ shared-ptr


【解决方案1】:

shared_ptr 传递给const&amp; 的优点是引用计数不必先增加再减少。因为这些操作必须是线程安全的,所以它们可能很昂贵。

您说得很对,您可以通过引用传递链,这会导致链的头部无效。这曾经发生在我的一个真实世界项目中,具有真实世界的后果。一个函数在容器中找到了shared_ptr,并将对它的引用传递到调用堆栈中。调用堆栈深处的一个函数从容器中删除了该对象,导致所有引用突然引用了一个不再存在的对象。

因此,当您通过引用传递某些内容时,调用者必须确保它在函数调用的整个生命周期内都存在。如果这是一个问题,请不要使用引用传递。

(我假设您有一个用例,其中有一些特定的原因通过 shared_ptr 而不是通过引用。最常见的原因是调用的函数可能需要延长对象的生命周期。 )

更新:为感兴趣的人提供有关该错误的更多详细信息:该程序具有共享的对象并实现了内部线程安全。它们被保存在容器中,延长它们的生命周期的函数很常见。

这种特殊类型的对象可以存在于两个容器中。一个是活动的,一个是不活动的。一些操作对活动对象起作用,一些对非活动对象起作用。当在非活动对象上接收到使其处于活动状态的命令而该对象的唯一 shared_ptr 由非活动对象的容器保存时,就会发生错误情况。

非活动对象位于其容器中。通过引用将容器中对shared_ptr 的引用传递给命令处理程序。通过一系列引用,这个shared_ptr 最终到达了意识到这是一个必须激活的非活动对象的代码。该对象已从非活动容器中移除(这破坏了非活动容器的 shared_ptr)并添加到活动容器中(这添加了对传递给“add”例程的 shared_ptr 的另一个引用)。

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

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

【讨论】:

  • 这正是我担心共享 shared_ptr 的情况。
  • 能否先将对象添加到活动对象中,然后再将其从非活动对象中删除?
  • @gnasher729 我的回忆是,在我们的特殊情况下,这是不可能的,因为这需要一个中间点,即对象处于非法状态并且没有持有任何锁。但这是我们特殊情况的一个怪癖。否则,在删除之前添加会解决它。
  • “调用它的代码并不真正知道它可以删除它所引用的对象”——我会说这是失败点。如果您有来自容器的shared_ptr,您知道该容器可能会被修改,但不知道要进行哪些修改,那么您不能传递对该@987654333 的引用@。事实上,如果我们将上一句中的shared_ptr 替换为int,情况也是如此,如果有人问“我注意到通过引用传递整数比值更快,我是否应该盲目替换所有通话?”。没有。
  • 所以我认为这个答案是正确的,请注意不要仅仅因为对象的复制成本很高,就经常将所有按值传递更改为按引用传递。原因并非特定于shared_ptr 的事实并不能阻止它成为不这样做的一个关键原因:-) 你不能只是传递对你无法确保其生命周期的事物的引用,无论如何昂贵的。你的容器示例可能出错了,因为有人假设“啊,有一个 shared_ptr 参与,生命周期都会很好,因为 shared_ptr 正在做它的事情”,而事实并非如此。
【解决方案2】:

首先,不要将shared_ptr 传递到调用链中,除非其中一个被调用函数可能会存储它的副本。传递对被引用对象的引用,或指向该对象的原始指针,或者可能是一个框,具体取决于它是否是可选的。

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

关于

价格降低了代码的安全性,对吧?

没有。代价是天真生成的机器代码的额外间接,但编译器管理它。因此,这一切都是为了避免编译器无法为您优化的微小但完全不必要的开销,除非它是超级智能的。

正如 David Schwarz 在他的回答中举例说明的那样,当您通过引用 const 传递 别名问题时,您调用的函数依次更改或调用更改原始对象的函数,是可能的。根据墨菲定律,它将发生在最不方便的时间,以最大的成本,以及最复杂的难以理解的代码。但无论参数是string 还是shared_ptr 或其他什么,都是如此。令人高兴的是,这是一个非常罕见的问题。但请记住,用于传递 shared_ptr 实例。

【讨论】:

  • 您的观点是,引用更改的这个问题不是shared_ptr 特定的问题,这是一个非常重要的问题!我认为,在答案中,它最能指出实际问题。
【解决方案3】:

首先两者之间存在语义上的区别: 按值传递共享指针表明您的函数将获得底层对象所有权的一部分。 除了强制此函数的用户使用 shared_ptr 之外,将 shared_ptr 作为 const 引用传递并不表示仅通过 const 引用(或原始指针)传递底层对象的任何意图。所以大部分都是垃圾。

只要它们在语义上不同,比较它们的性能影响是无关紧要的。

来自https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/

除非您愿意,否则不要将智能指针作为函数参数传递 使用或操作智能指针本身,例如共享或 转让所有权。

这一次我完全同意 Herb :)

另一句同样的引述,更直接地回答了这个问题

指南:仅使用非常量 shared_ptr& 参数来修改 shared_ptr。仅当您不确定是否会复制并共享所有权时,才使用 const shared_ptr& 作为参数;否则使用 * 代替(或者如果不可为空,则使用 &)

【讨论】:

  • 确实如此,但与问题无关。
  • 我认为这非常相关,但我添加了更明确的引用。请注意,对我来说更明确的引用暗示了一个臭的设计,所以我倾向于说 const shared_ptr& 作为函数参数几乎没有用。
  • 问题是关于通过值传递 shared_ptr 而不是通过 const 引用传递它。您的答案中没有任何内容可以解决这个问题。
【解决方案4】:

正如C++ - shared_ptr: horrible speed 中所指出的,复制shared_ptr 需要时间。构造涉及原子增量和销毁原子减量,原子更新(无论是增量还是减量)可能会阻止许多编译器优化(内存加载/存储不能跨操作迁移)并且在硬件级别涉及 CPU 缓存一致性协议以确保整个缓存行由进行修改的核心拥有(独占模式)。

所以,你是对的,std::shared_ptr&lt;T&gt; const&amp; 可以用作对 std::shared_ptr&lt;T&gt; 的性能改进。


您也说对了,指针/引用在理论上存在由于一些别名而悬空的风险。

话虽如此,任何 C++ 程序中都存在潜在风险:任何一次使用指针或引用都是一种风险。我认为与T&amp;T const&amp;T*、...的总使用次数相比,std::shared_ptr&lt;T&gt; const&amp; 的少数出现应该是水中的一滴水。


最后,我想指出传递shared_ptr&lt;T&gt; const&amp;奇怪。以下情况比较常见:

  • shared_ptr&lt;T&gt;:我需要一份shared_ptr
  • T*/T const&amp;/T&amp;/T const&amp;:我需要T的(可能为空)句柄

下一种情况不太常见:

  • shared_ptr&lt;T&gt;&amp;:我可以重新安装shared_ptr

但是通过shared_ptr&lt;T&gt; const&amp;?合法用途非常罕见。

传递shared_ptr&lt;T&gt; const&amp; 而你想要的只是对T 的引用是一种反模式:当用户可以通过另一种方式分配T 时,你会强制用户使用shared_ptr!大多数时候(99,99..%),您不应该关心T 是如何分配的。

您会传递shared_ptr&lt;T&gt; const&amp; 的唯一情况是,如果您不确定是否需要副本,并且因为您已经分析了程序并表明这个原子增量/减量是您决定的瓶颈将副本的创建推迟到需要的情况下。

这是一种极端情况,任何使用shared_ptr&lt;T&gt; const&amp; 的行为都应该受到最高程度的怀疑。

【讨论】:

  • 您对其他问题的引用很好地回答了我关于性能的隐含问题,到目前为止还没有涉及到。
  • @MikeLischke:正是我包含它的原因;)
  • 没有什么奇怪的,在有效的 C++ 程序中不存在空引用。很高兴与 lthe 链接到更多信息。所以,+ 和 -,= 没有投票权。 :)
  • @Cheersandhth.-Alf:空引用指的是指针(可能为空),它们不是标准中的“引用”......我可以看到这是多么令人困惑:x I'改成“handle”了,这样更好吗?
【解决方案5】:

如果您的方法中不涉及所有权修改,则您的方法通过复制或通过 const 引用获取 shared_ptr 没有任何好处,它会污染 API 并可能产生开销(如果通过复制传递)

干净的方法是通过 const ref 或 ref 传递基础类型,具体取决于您的用例

void doSomething(const T& o) {}

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

方法调用过程中不能释放底层指针

【讨论】:

  • 这没有回答所提出的问题。问题是关于通过值传递 shared_ptr 而不是通过 const 引用传递 shared_ptr
  • 问题是通过 const & 传递 shared_ptr 绝对没有用,除非你想污染 API,如果在你的方法范围内你会创建一些,通过 copy 的 shared_ptr 可能很有用将比原始 shared_ptr 寿命更长的包装器
【解决方案6】:

如果目标函数是同步的并且仅在执行期间使用参数并且在返回时不再需要它,我认为传递const &amp; 是完全合理的。在这里节省增加引用计数的成本是合理的——因为在这些有限的情况下你并不需要额外的安全——只要你了解其中的含义并确定代码是安全的。

这与函数需要保存参数(例如在类成员中)以供以后重新引用时相反。

【讨论】:

  • “这与函数需要保存参数(例如在类成员中)以供以后重新引用时相反。”是没有意义的。没有什么可以阻止您复制通过引用传递给 constshared_ptr
  • 确实没有什么能阻止您复制const &amp;。但是,我认为如果这是您的意图,让函数按值获取参数会使事情变得更清楚。这种清晰性也不会以操作速度较慢为代价:因为如果需要,您始终可以将 shared_ptr 按值参数移动到函数中的最终目的地,因此您不需要额外的副本
  • 是的,当函数没有反过来传递shared_ptr时。事实上,移动价值传递是 Herb 推荐的,所以如果这就是你的意思,那么我错了,它没有意义。但是当你有一个调用链时,即使只有两个级别,成本也会相反(复制+复制+移动与引用+引用+复制+移动),如果必须选择单个,则有利于通过引用 const约定。
  • 如果代码以某种方式组织(即链接调用是shared_ptr 在任何给定级别的最后一次使用),即使在链中,您也可以只使用@987654329 @您的共享指针在链中,这避免了您提到的额外副本。话虽如此,我相信在某些情况下您的方法更合适。我只是不同意我最初的评论“毫无意义”的最初观点
  • 通常不能只是move对象在调用链中,因为通常调用不是使用对象的块中的最后一个位置。所以这不能作为一般惯例。这是一个非常特殊的情况。
猜你喜欢
  • 2017-07-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-21
  • 2017-10-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多