【问题标题】:Should we pass a shared_ptr by reference or by value?我们应该通过引用还是按值传递 shared_ptr ?
【发布时间】:2023-04-07 22:46:01
【问题描述】:

当一个函数采用 shared_ptr(来自 boost 或 C++11 STL)时,您是否传递了它:

  • 通过常量引用:void foo(const shared_ptr<T>& p)

  • 或按值:void foo(shared_ptr<T> p)?

我更喜欢第一种方法,因为我怀疑它会更快。但这真的值得吗?还是有其他问题?

能否请您说明您选择的原因,或者如果是,您认为这无关紧要的原因。

【问题讨论】:

  • 问题是那些不等价的。参考版本尖叫“我要别名一些shared_ptr,如果我愿意,我可以更改它。”而价值版本说“我要复制你的shared_ptr,所以虽然我可以更改它你永远不会知道。)一个 const-reference 参数是真正的解决方案,它说“我要给一些 shared_ptr 起别名,我保证不会改变它。”(这与按值语义极为相似!)
  • 嘿,我很想听听你们关于返回 shared_ptr 班级成员的意见。你是通过 const-refs 来做的吗?
  • 第三种可能性是在 C++0x 中使用 std::move(),这会交换 shared_ptr
  • @Johannes:我会通过 const-reference 返回它,以避免任何复制/引用计数。再说一次,我通过 const-reference 返回所有成员,除非它们是原始的。
  • 在 lambdas 中不应该遵循通过 ref 传递 shared_ptr 的习惯。如果它在其他地方被破坏(通过 ref 不会增加 ref 计数),您的回调/lambda 可能会崩溃。 OTOH,在 lambdas 中按值传递它也是危险的,并且可能导致内存泄漏。相反,我们应该将 weak_ptr 传递给 shared_ptr。

标签: c++ c++11 boost shared-ptr


【解决方案1】:

Scott、Andrei 和 Herb 在Ask Us Anything 会议期间C++ and Beyond 2011 讨论并回答了这个问题。从 4:34 on shared_ptr performance and correctness 观看。

简而言之,没有理由按值传递,除非目标是共享对象的所有权(例如,在不同的数据结构之间,或在不同的线程之间)。

除非您可以按照 Scott Meyers 在上面链接的谈话视频中解释的那样对其进行移动优化,但这与您可以使用的实际 C++ 版本有关。

GoingNative 2012 会议的Interactive Panel: Ask Us Anything! 期间发生了此讨论的重大更新,值得关注,尤其是来自22:50

【讨论】:

  • 但如此处所示,通过值传递更便宜:stackoverflow.com/a/12002668/128384 不应该也考虑到这一点(至少对于构造函数参数等,其中 shared_ptr 将被设为班级成员)?
  • @stijn 是也不是。您指出的问答是不完整的,除非它阐明了它所引用的 C++ 标准的版本。传播一般的从不/总是规则非常容易,这只是误导。除非读者花时间熟悉 David Abrahams 的文章和参考资料,或者考虑发布日期与当前 C++ 标准。因此,考虑到发布时间,我的答案和您指出的答案都是正确的。
  • 我迟到了,但我想按值传递 shared_ptr 的原因是它使代码更短更漂亮。严重地。 Value* 简短易读,但它很糟糕,所以现在我的代码充满了const shared_ptr<Value>&,而且它的可读性明显降低,而且……不那么整洁。以前的 void Function(Value* v1, Value* v2, Value* v3) 现在是 void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3),人们还可以接受吗?
  • @Alex 常见的做法是在课后立即创建别名(typedef)。对于您的示例:class Value {...}; using ValuePtr = std::shared_ptr<Value>; 然后您的功能变得更简单:void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3) 并且您将获得最佳性能。这就是你使用 C++ 的原因,不是吗? :)
  • 我仍然不明白除非子句:“除非目标是共享对象的所有权”——shared_ptr 不总是这样吗?此外,值语义更“自然”。通过引用传递总是需要证明,而不是相反。为什么要通过引用传递?
【解决方案2】:

这里是Herb Sutter's take

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

指南:表明一个函数将存储和共享一个函数的所有权 使用按值 shared_ptr 参数的堆对象。

指南:使用 non-const shared_ptr& 参数仅用于修改shared_ptr。用一个 const shared_ptr& 仅当您不确定是否 不是你会复制并分享所有权;否则使用小部件* 而是(或者如果不可为空,则为小部件&)。

【讨论】:

  • 感谢 Sutter 的链接。这是一篇很棒的文章。我不同意他对 widget* 的看法,如果 C++14 可用,我更喜欢 optional。小部件* 与旧代码过于模糊。
  • +1 包括 widget* 和 widget& 作为可能性。只是为了详细说明,当函数不检查/修改指针对象本身时,传递 widget* 或 widget& 可能是最好的选择。该接口更通用,因为它不需要特定的指针类型,并且避免了 shared_ptr 引用计数的性能问题。
  • 我认为这应该是今天公认的答案,因为第二个准则。它显然使当前接受的答案无效,即:没有理由按值传递。
【解决方案3】:

我个人会使用const 参考。没有必要为了函数调用而增加引用计数来再次减少它。

【讨论】:

  • 我没有否决您的答案,但在此之前,这两种可能性各有利弊。了解和讨论这些优点和缺点会很好。之后每个人都可以自己做决定。
  • @Danvil:考虑到shared_ptr 的工作原理,不通过引用传递的唯一可能缺点是性能略有下降。这里有两个原因。 a) 指针别名特性意味着指向数据的指针加上一个计数器(对于弱引用可能为 2)被复制,因此复制数据轮次的成本略高。 b) 原子引用计数比普通的旧增量/减量代码稍慢,但为了线程安全是必需的。除此之外,这两种方法在大多数意图和目的上都是相同的。
【解决方案4】:

通过const 参考,它更快。如果你需要存储它,比如说在某个容器中,ref. count 将通过复制操作自动增加。

【讨论】:

  • 投反对票,因为它的意见没有任何数字支持。
  • @kwesolowski 答案提供了为什么 const 引用更快(即没有不必要的引用计数递增/递减)的分析原因。它是基准测试的替代方法。
【解决方案5】:

我运行了下面的代码,一次是foo 通过const& 获取shared_ptr,另一次是foo 通过值获取shared_ptr

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

在我的英特尔酷睿 2 四核 (2.4GHz) 处理器上使用 VS2015、x86 版本构建

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

按值复制版本慢了一个数量级。
如果您从当前线程同步调用函数,则首选const&amp; 版本。

【讨论】:

  • 您能说一下您使用的编译器、平台和优化设置吗?
  • 我很好奇当优化开启时,你是否会得到相同的结果
  • 在我的 2012 MacBook Pro(2.5 GHz Intel Core i7)上使用 clang++ -O3 -std=c++11 分别得到 42 毫秒和 179227 毫秒。
  • 优化没有多大帮助。问题是副本上的引用计数锁定争用。
  • 这不是重点。这样的foo() 函数甚至不应该首先接受共享指针,因为它没有使用这个对象:它应该接受int&amp; 并执行p = ++x;,从main() 调用foo(*p);。函数在需要对其执行某些操作时接受智能指针对象,并且大多数情况下,您需要做的是将其 (std::move()) 移动到其他位置,因此按值参数没有成本。
【解决方案6】:

从 C++11 开始,您应该比您想象的更频繁地按值而不是 const&

如果你使用 std::shared_ptr(而不是底层类型 T),那么你这样做是因为你想用它做点什么。

如果您想在某处复制它,通过复制获取它并在内部使用 std::move 比通过 const& 获取它然后再复制它更有意义。这是因为您允许调用者在调用函数时依次选择 std::move shared_ptr,从而为自己节省一组递增和递减操作。或不。也就是说,函数的调用者可以在调用函数后决定他是否需要std::shared_ptr,取决于是否移动。如果您通过 const& 传递,这是无法实现的,因此最好按值获取。

当然,如果调用者都需要他的 shared_ptr 更长的时间(因此不能 std::move 它)并且你不想在函数中创建一个普通副本(比如你想要一个弱指针,或者你只是有时想复制它,这取决于某些条件),那么 const& 可能仍然是可取的。

例如,你应该这样做

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

结束

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

因为在这种情况下,您总是在内部创建副本

【讨论】:

  • 像 Jason Turner 这样的人现在说,尽管过度使用 std::move 是一种“代码味道”,应该尽可能避免。
【解决方案7】:

最近有一篇博文:https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

所以这个问题的答案是:(几乎)永远不要经过const shared_ptr&lt;T&gt;&amp;
只需传递底层类即可。

基本上唯一合理的参数类型是:

  • shared_ptr&lt;T&gt; - 修改并取得所有权
  • shared_ptr&lt;const T&gt; - 不要修改,拥有所有权
  • T&amp; - 修改,无所有权
  • const T&amp; - 不要修改,没有所有权
  • T - 不要修改,没有所有权,复制成本低

正如@accel 在https://stackoverflow.com/a/26197326/1930508 中指出的那样,Herb Sutter 的建议是:

仅当您不确定是否会复制并共享所有权时,才使用 const shared_ptr& 作为参数

但在多少情况下您不确定?所以这种情况很少见

【讨论】:

  • IMO 这是正确的答案之一,但 是最简洁的答案。
【解决方案8】:

众所周知,按值传递 shared_ptr 是有代价的,应尽可能避免。

The cost of passing by shared_ptr

大多数时候通过引用传递 shared_ptr ,甚至通过 const 引用传递更好。

cpp 核心指南对传递 shared_ptr 有一个特定的规则

R.34: Take a shared_ptr parameter to express that a function is part owner

void share(shared_ptr<widget>);            // share -- "will" retain refcount

真正需要按值传递 shared_ptr 的一个例子是调用者将共享对象传递给异步被调用者 - 即调用者在被调用者完成其工作之前超出范围。被调用者必须通过按值获取 share_ptr 来“延长”共享对象的生命周期。在这种情况下,传递对 shared_ptr 的引用是行不通的。

将共享对象传递给工作线程也是如此。

【讨论】:

    【解决方案9】:

    不知道原子增量和减量所在的 shared_copy 复制操作的时间成本,我遇到了更高的 CPU 使用率问题。没想到原子增量和减量会花费这么多成本。

    根据我的测试结果,int32 原子增量和减量比非原子增量和减量需要 2 或 40 倍。我在 3GHz Core i7 和 Windows 8.1 上得到了它。前一个结果在没有发生争用时出现,而后一个结果在出现争用的可能性很高时出现。我记住原子操作最后是基于硬件的锁。锁就是锁。发生争用时对性能不利。

    遇到这种情况,我总是使用 byref(const shared_ptr&) 而不是 byval(shared_ptr)。

    【讨论】:

      【解决方案10】:

      shared_ptr 不够大,它的构造函数\析构函数也没有做足够的工作来使副本产生足够的开销来关心按引用传递与按副本传递的性能。

      【讨论】:

      • @stonemetal: 在创建新的 shared_ptr 时原子指令呢?
      • 它是一种非 POD 类型,因此在大多数 ABI 中,即使是“按值”传递它实际上也会传递一个指针。根本不是字节的实际复制问题。正如您在 asm 输出中所见,按值传递 shared_ptr&lt;int&gt; 会占用 100 条 x86 指令(包括昂贵的 locked 指令以原子地增加/减少引用计数)。传递常量 ref 与传递指向任何东西的指针相同(在 Godbolt 编译器资源管理器的此示例中,尾调用优化将其转换为简单的 jmp 而不是调用:godbolt.org/g/TazMBU)。
      • TL:DR:这是 C++,其中复制构造函数可以做更多的工作,而不仅仅是复制字节。这个答案完全是垃圾。
      • stackoverflow.com/questions/3628081/shared-ptr-horrible-speed 作为一个示例,共享指针按值传递与按引用传递,他发现运行时间差异约为 33%。如果您正在处理性能关键代码,那么裸指针可以让您获得更大的性能提升。因此,如果您记得,请务必通过 const ref,但如果您不这样做,那也没什么大不了的。如果您不需要它,不要使用 shared_ptr 更为重要。
      猜你喜欢
      • 2012-01-13
      • 1970-01-01
      • 1970-01-01
      • 2012-04-28
      • 2015-04-10
      • 1970-01-01
      • 2016-12-13
      • 2020-04-01
      • 2016-06-11
      相关资源
      最近更新 更多