我有点惊讶,这里没有非常清楚和明确地回答这个问题,也没有在我容易偶然发现的任何地方回答。虽然我对这些东西很陌生,但我认为可以说以下内容。
这种情况是一个调用函数,它构建一个unique_ptr<T>
值(可能通过将调用的结果强制转换new
为就像这里发生的那样进入a vector
)。为了表明调用者已经获得了所有权,并准备放弃它,传递一个unique_ptr<T>
值是适当的。据我所知,传递这样一个值的三种合理模式。
- 按值传递,如
add(unique_ptr<B> b)
问题中所示。
- 通过非
const
左值引用传递,如add(unique_ptr<B>& b)
- 通过右值引用传递,如
add(unique_ptr<B>&& b)
通过const
左值引用传递是不合理的,因为它不允许被调用的函数获得所有权(并且const
右值引用会比这更愚蠢;我什至不确定它是否被允许)。
就有效代码而言,选项 1 和 3 几乎是等价的:它们强制调用者写入一个右值作为调用的参数,可能通过在调用中包装一个变量std::move
(如果参数已经是一个右值,即未命名)就像从 的结果中进行的强制转换一样new
,这不是必需的)。然而,在选项 2 中,不允许传递右值(可能来自std::move
),并且必须使用命名变量调用函数(当传递来自 的强制转换时,必须先分配给变量)。unique_ptr<T>
new
当std::move
确实使用时,unique_ptr<T>
在调用者中保存该值的变量在概念上被取消引用(转换为右值,分别转换为右值引用),并且此时放弃所有权。在选项 1 中,取消引用是真实的,并且值被移动到传递给被调用函数的临时值(如果调用函数将检查调用者中的变量,它会发现它已经持有一个空指针)。所有权已经转移,调用者不可能决定不接受它(对参数不做任何事情会导致指向的值在函数退出时被销毁;release
在参数上调用方法会阻止这种情况,但只会导致内存泄漏)。令人惊讶的是,选项 2. 和 3. 在语义上在函数调用期间等效,尽管它们对调用者需要不同的语法。如果被调用函数将参数传递给另一个采用右值的函数(例如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)); } // or even better use emplace_back
};
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))); // unnamed argument does not need std::move
}
如所写,输出为
storing 123
---
storing 4567
dropping 123
dropping 4567
请注意,值按存储在向量中的顺序销毁。尝试更改方法的原型add
(必要时调整其他代码以使其编译),以及它是否实际上传递了它的参数b
。可以获得输出线的几种排列。