是否需要通过引用传递智能指针对象?

问题描述 投票:0回答:2

假设我有一个类,其中包含一个智能指针作为其成员变量:

class B;

class A {
 public:
  A(const std::shared_ptr<B>& b) : b_(b) {}  // option1: passing by reference
  A(std::shared_ptr<B> b) : b_(b) {}  // option2: passing by value
  std::shared_ptr<B> b_;
};

A的构造函数有两种选择:通过智能指针构造和通过智能指针的引用构造。

这两种方法各有什么优缺点?

复制智能指针是否浪费?

c++ c++11 smart-pointers
2个回答
9
投票

最好的选择是选项#3:

A(std::shared_pointer<B> b) : b_(std::move(b)) {}  // option3: passing by value and move

option1
不同,当
shared_ptr
是根据传递给
A
的构造函数的纯右值构造时,它不会执行不必要的复制。与
option2
不同,它不会在内部执行不必要的复制。

费用为:

  1. 当传递纯右值时,它执行单个构造和单个移动构造(这对于
    shared_ptr
    来说更有效,因为它避免了在复制和销毁源时需要操作引用计数的方式)
  2. 当传递任何其他 r 值(例如
    A(std::move(callers_ptr))
    )时,它会移动构造两次(再次避免任何 refcnt 操作),但同样,没有副本
  3. 当传递左值时,它会复制一次(复制到参数中,在构造对象之前获取所有权),然后廉价地移动到成员中。在这种情况下,调用者保留所有权,同时也给您单独的所有权,因此单个副本是不可避免的。

所以成本是:

  • prvalue:一招(加上原版必要的实际构建
    shared_ptr
  • 其他 r 值:两次移动
  • l-值:一次移动(加上获得所有权所需的副本)

为了比较,每个场景中的选项 #1 需要:

  • 纯右值:一份副本(加上原始的必要构造
    shared_ptr
  • 其他 r 值:一份副本(也可能是额外的移动;在这种情况下,对于
    const
    引用生命周期扩展的 C++ 规则,不是 100%;无论哪种方式,考虑到
     中涉及的原子,一份副本都比两次移动更糟糕shared_ptr
  • l-value:只是获得所有权所必需的副本(选项#3的改进,增加了一次移动,但移动很便宜,因此在其他情况下保存副本更为重要)
每个场景中的选项 #2 与选项 #3 完全相同,但每个场景中的一个移动变成了副本(因此选项 #2 在每个场景中客观上更差);添加

std::move 将选项 #2 更改为选项 #3 是一个纯粹的胜利。

所以,是的,如果调用者
总是

保留自己对

shared_ptr的所有权,同时也给予A自己的所有权,永远不会将其所有权转移给新A。但您保存的每一步都只是几个非原子指针分配;无论哪种方式,您都可以将指针从源复制到目标,通过移动将

NULL
移出源,而保存副本意味着您可以避免通过指向非本地控制块的指针原子地增加引用计数(可能比本地非原子指针分配昂贵一个数量级)。

注意:有一个选项#4,正如 Nathan 在评论中提到的那样,严格来说性能更高,
使用完美转发构造函数在每种情况下跳过移动操作
。缺点是代码变得更加复杂,并且如果用例更加复杂(不仅仅是单个简单的
shared_ptr
成员),那么您可能会遇到与(通常不是

noexcept
)内发生的构造/复制操作有关的潜在问题。构造函数而不是调用方,因此仅部分构造对象时发生异常的风险会增加。只要所有非移动操作发生在构造函数外部(意味着它们是针对参数完成的),构造函数本身通常可以是

noexcept 并且避免需要处理中间初始化异常的可能性。

我认为 Herb Sutter 的这个
答案
足够清楚了
但是指南很简单——不要传递智能指针,除非你想使用/修改智能指针本身(就像任何对象一样)。最主要的是,按值传递/*/& 仍然很好,仍然应该主要使用。只是我们现在有一些习惯用语来表达函数签名中的所有权转移,特别是按值传递 unique_ptr 意味着“下沉”,按值传递共享_ptr 意味着“将共享所有权”。差不多就这些了。

    


0
投票
© www.soinside.com 2019 - 2024. All rights reserved.