我应该返回一个右值引用(通过std :: move'ing)吗?

时间:2012-11-17 13:01:56

标签: c++ return rvalue-reference stdmove

A C++Next blog post

A compute(…)
{
    A v;
    …
    return v;
}

如果A具有可访问的副本或移动构造函数,则编译器可以选择忽略该副本。否则,如果A有移动构造函数,则移动v。否则,如果A具有复制构造函数,则会复制v。  否则,将发出编译时错误。

我认为我应该总是在没有std::move的情况下返回值 因为编译器能够找出用户的最佳选择。但另一个例子来自博客文章

Matrix operator+(Matrix&& temp, Matrix&& y)
  { temp += y; return std::move(temp); }

此处std::move是必需的,因为必须将y视为函数内的左值。

啊,在研究这篇博文之后,我的脑袋几乎爆炸了。我尽力理解推理,但学的越多,我就越困惑。为什么我们应该在std::move的帮助下返回值?

4 个答案:

答案 0 :(得分:22)

所以,假设你有:

A compute()
{
  A v;
  …
  return v;
}

你正在做:

A a = compute();

此表达式涉及两种传输(复制或移动)。首先,必须将函数中由v表示的对象转移到函数的结果,即compute()表达式捐赠的值。让我们调用Transfer 1.然后,转移此临时对象以创建由a表示的对象 - 转移2。

在许多情况下,编译器可以省略转移1和转移2 - 对象v直接在a的位置构建,不需要转移。在此示例中,编译器必须使用传输1的命名返回值优化,因为返回的对象已命名。但是,如果我们禁用复制/移动省略,则每次传输都需要调用A的复制构造函数或其移动构造函数。在大多数现代编译器中,编译器将看到v即将被销毁,它将首先将其移动到返回值中。然后,此临时返回值将移至a。如果A没有移动构造函数,则会为两次传输复制它。

现在让我们来看看:

A compute(A&& v)
{
  return v;
}

我们返回的值来自传递给函数的引用。编译器并不只是假设v是临时的,并且可以从它移动 1 。在这种情况下,转移1将是一个副本。然后转移2将是一个移动 - 这是可以的,因为返回的值仍然是临时的(我们没有返回引用)。但是既然我们知道我们已经采取了一个我们可以移动的对象,因为我们的参数是一个右值引用,我们可以明确告诉编译器将v视为临时与std::move

A compute(A&& v)
{
  return std::move(v);
}

现在转移1和转移2都将移动。


1 编译器没有自动将v(定义为A&&)视为右值的原因之一是安全性。弄清楚它并不是太愚蠢。一旦对象具有名称,就可以在整个代码中多次引用它。考虑:

A compute(A&& a)
{
  doSomething(a);
  doSomethingElse(a);
}

如果a被自动视为左值,则doSomething可以自由地删除其内容,这意味着传递给a的{​​{1}}可能无效。即使doSomethingElse按值获取其参数,该对象也会从下一行中移出,因此无效。为了避免这个问题,命名的右值引用是左值。这意味着,当doSomething被调用时,doSomething最坏的情况将被复制,如果不是仅仅通过左值引用 - 它仍将在下一行中有效。

a的作者来说,"好吧,现在我允许移动这个值,因为我肯定知道它是一个临时对象" 。你这样说compute。例如,您可以为std::move(a)提供一份副本,然后允许doSomething从中移出:

doSomethingElse

答案 1 :(得分:5)

功能结果的隐式移动仅适用于自动对象。右值参考参数不表示自动对象,因此在这种情况下您必须明确请求移动。

答案 2 :(得分:5)

第一个利用NVRO甚至比移动更好。 没有副本比廉价副本更好。

第二个不能利用NVRO。假设没有省略,return temp;将调用复制构造函数,return std::move(temp);将调用移动构造函数。现在,我相信其中任何一个都有同等的潜力被淘汰,所以你应该选择更便宜的,如果没有被淘汰,那就是使用std::move

答案 3 :(得分:1)

在C ++ 17和更高版本中

C ++ 17更改了值类别的定义,以使保证可以描述您所描述的复制省略类型(请参见:How does guaranteed copy elision work?)。因此,在这种情况下,如果您使用C ++ 17或更高版本编写代码,则绝对不要通过std::move()返回。