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

nic*_*ick 6 c++ smart-pointers c++11

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

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_;
};
Run Code Online (Sandbox Code Playgroud)

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

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

复制智能指针是浪费吗?

Sha*_*ger 9

最好的选择是选项#3:

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

与 不同的是,当 是从传递给 的构造函数的纯右值构造option1时,它不会执行不必​​要的复制。与 不同的是,它不会在内部执行不必要的复制。shared_ptrAoption2

费用为:

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

所以成本是:

  • prvalue:一步(加上原始必要的实际构造shared_ptr
  • 其他 r 值:两步
  • l 值:一步(加上获得所有权所需的副本)

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

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

每个场景中的选项 #2 与选项 #3 完全相同,但每个场景中的一个移动变成了副本(因此选项 #2 在每个场景中客观上更差);添加std::move将选项 #2 更改为选项 #3 是一个纯粹的胜利。

所以,是的,如果调用者始终保留自己的所有权,同时也给予其自己的所有权,而不将其所有权转移给新的,那么选项#1可能稍微更有效。但您保存的每一步都只是几个非原子指针分配;无论哪种方式,您都可以将指针从源复制到目标,通过 move 将 ing 添加到源之外,而保存副本意味着您可以避免通过指向非本地控制块的指针原子地增加引用计数(可能是比本地非原子指针分配昂贵一个数量级)。shared_ptrAANULL


注意:正如 Nathan 在评论中提到的,有一个选项#4,它的性能更高,在每种情况下使用完美转发构造函数来跳过移动操作。缺点是代码变得更加复杂,并且如果用例更加复杂(不仅仅是单个简单的shared_ptr成员),那么您可能会遇到与noexcept构造函数中发生的(通常不是)构造/复制操作相关的潜在问题,而不是在构造函数中调用方,因此仅部分构造对象时发生异常的风险会增加。只要所有非移动操作发生在构造函数外部(意味着它们是针对参数完成的),构造函数本身通常就可以避免noexcept需要处理中间初始化异常的可能性。

  • 完美转发不是最好的选择吗? (2认同)
  • @NathanOliver 您可以通过这种方式保存的额外移动操作可能并不重要。移动shared_ptr应该很便宜,例如[这里](https://github.com/llvm/llvm-project/blob/main/libcxx/include/__memory/shared_ptr.h#L613)对于libc++来说它只需要复制和将两个指针归零。对于非内联调用,按值传递与按引用传递是否更有效可能取决于细节。但实现完美转发并不那么简单,需要 SFINAE 或多个定义。所以我想说这可能不值得。 (2认同)