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

Should I return an rvalue reference (by std::move'ing)?

本文关键字:move ing std 引用 返回 一个 我应该 通过      更新时间:2023-10-16

一篇C++下一篇博客文章说

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的帮助下返回值?

所以,假设您有:

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

你在做:

A a = compute();

此表达式中涉及两种传输(复制或移动)。首先,函数中由v表示的对象必须转换为函数的结果,即compute()表达式提供的值。让我们称之为转移1。然后,这个临时对象被转移以创建由a-Transfer 2表示的对象。

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

现在我们来看一下:

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

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

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

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


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

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

如果a被自动处理为右值,那么doSomething将可以自由地将其内脏撕裂,这意味着传递给doSomethingElsea可能是无效的。即使doSomething按值取其参数,对象也会从中移出,因此在下一行中无效。为了避免这个问题,命名的右值引用是左值。这意味着当调用doSomething时,a最坏情况下将从中复制,如果不是仅由左值引用获取的话,它在下一行中仍然有效。

这取决于compute的作者说,"好吧,现在我允许从中移动这个值,因为我确信它是一个临时对象"。你可以说std::move(a)。例如,您可以给doSomething一个副本,然后允许doSomethingElse从中移动:

A compute(A&& a)
{
doSomething(a);
doSomethingElse(std::move(a));
}

函数结果的隐式移动仅适用于自动对象。右值引用参数并不表示自动对象,因此在这种情况下必须显式请求移动。

第一个利用NVRO,它甚至比移动更好<没有副本比便宜的好。>

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

在C++17及更高版本中

C++17更改了值类别的定义,使您描述的复制省略类型为保证的(请参阅:保证的复制省略是如何工作的?)。因此,如果您用C++17或更高版本编写代码,那么在这种情况下,您绝对不应该通过std::move()'ing返回。