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