我有点惊讶于这里没有很清楚和明确地回答,也没有任何我容易发现的地方。虽然我对这个东西还比较新,但我认为以下内容可以说出来。
情况是一个调用函数构建了一个unique_ptr<T>
值(可能通过将结果从调用new
的结果强制转换而来),并且想将其传递给一些将拥有该对象指针的函数(例如将其存储在数据结构中,如此处存储在vector
中)。为了表明调用者已经获得了所有权,并准备放弃它,传递unique_ptr<T>
值即可。就我所见,有三种合理的传递这样的值的模式。
- 按值传递,如问题中的
add(unique_ptr<B> b)
。
- 通过非
const
左值引用传递,如add(unique_ptr<B>& b)
- 通过右值引用传递,如
add(unique_ptr<B>&& b)
通过const
左值引用传递是不合理的,因为它不允许被调用的函数获取所有权(而const
右值引用会更傻比; 我甚至不确定它是否被允许)。
就有效代码而言,选项1和3几乎是等效的:它们强制调用者将rvalue作为参数传递给调用,可能通过将变量包装在对std :: move
的调用中来实现(如果参数已经是rvalue,即无名字,例如从new
的结果中转化而来,则不需要这样做)。然而,在选项2中,不能传递rvalue(可能来自于std::move
),并且必须使用命名的unique_ptr<T>
变量调用该函数(当传递从new
转换而来的值时,必须首先赋值给一个变量)。
当确实使用
std::move
时,调用方中持有
unique_ptr<T>
值的变量被概念化地取消引用(转换为rvalue,分别转换为rvalue引用),并在此时放弃所有权。在选项1中,取消引用是真实的,并且该值被移动到一个临时变量中,该变量传递给被调用的函数(如果调用函数检查调用方中的变量,则会发现它已经保持了空指针)。所有权已经转移,调用方没有办法不接受它(什么都不做会导致指向的值在函数退出时被销毁;在参数上调用
release
方法将防止这种情况,但会导致内存泄漏)。令人惊讶的是,在函数调用期间,选项2和3在语义上是等效的,尽管它们对于调用方需要不同的语法。如果被调用的函数将参数传递给另一个接受rvalue的函数(例如
push_back
方法),则在两种情况下都必须插入
std::move
,这将在那一点转移所有权。如果被调用的函数忘记对参数进行任何操作,那么如果调用方持有其名称,则在选项2中仍将拥有该对象;尽管在情况3中,由于函数原型要求调用方同意释放所有权(通过调用
std::move
或提供临时对象),但实际转移所有权是延迟到函数调用之后的。总之,这些方法做:
- 强制调用方放弃所有权,并确保实际认领所有权。
- 强制调用方持有所有权,并准备好(通过提供非
const
引用)放弃所有权;然而,这不是显式的(不需要也不允许调用std::move
),并且不能确保夺取所有权。我认为这种方法意图相当不清楚,除非明确打算夺取所有权或不夺取所有权是被调用函数的自由裁量权(可以想象一些用途,但调用方需要注意)
- 强制调用方明确指示放弃所有权,如方法1(但实际上只在函数调用之后才延迟转移所有权)。
选项3在其意图上相当清晰;只要实际上获得了所有权,它对我来说是最好的解决方案。它比选项1稍微更有效率,因为没有将指针值移动到临时变量中(对std::move
的调用实际上只是类型转换,成本为零);如果指针在其内容实际移动之前通过多个中间函数传递,则这可能尤其重要。
下面是一些可供实验的代码。
class B
{
unsigned long val;
public:
B(const unsigned long& x) : val(x)
{ std::cout << "storing " << x << std::endl;}
~B() { std::cout << "dropping " << val << std::endl;}
};
typedef std::unique_ptr<B> B_ptr;
class A {
std::vector<B_ptr> vb;
public:
void add(B_ptr&& b)
{ vb.push_back(std::move(b)); }
};
void f() {
A a;
B_ptr b(new B(123)),c;
a.add(std::move(b));
std::cout << "---" <<std::endl;
a.add(B_ptr(new B(4567)));
}
输出结果如下:
storing 123
---
storing 4567
dropping 123
dropping 4567
注意,vector中存储的值按顺序被销毁。尝试更改方法add
的原型(根据需要调整其他代码使其编译),以及它是否实际传递其参数b
。可以获得多个输出行的排列。
add
的方式,就可以避免在主函数中使用std::move
,但这样做是不好的,因为调用add
的人没有查看add
源代码就无法确定他们是否仍然拥有该对象。 - Puppy