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

2022-01-19 00:00:00 return c++ rvalue-reference stdmove

A C++下一篇博文说过

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

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

If A has an accessible copy or move constructor, the compiler may choose to elide the copy. Otherwise, if A has a move constructor, v is moved. Otherwise, if A has a copy constructor, v is copied. Otherwise, a compile time error is emitted.

我认为我应该总是返回不带 std::move 的值因为编译器将能够为用户找出最佳选择.但在博客文章的另一个例子中

I thought I should always return the value without std::move because the compiler would be able to figure out the best choice for users. But in another example from the blog post

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

这里 std::move 是必要的,因为 y 必须被视为函数内的左值.

Here the std::move is necessary because y must be treated as an lvalue inside the function.

啊,读完这篇博文后,我的头都快炸了.我尽力去理解其中的道理,但我研究得越多,我就越困惑.为什么要在std::move的帮助下返回值?

Ah, my head almost blow up after studying this blog post. I tried my best to understand the reasoning but the more I studied, the more confused I became. Why should we return the value with the help of std::move?

推荐答案

所以,假设你有:

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

你正在做:

A a = compute();

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

There are two transfers (copy or move) that are involved in this expression. First the object denoted by v in the function must be transferred to the result of the function, i.e. the value donated by the compute() expression. Let's call that Transfer 1. Then, this temporary object is transferred to create the object denoted by a - Transfer 2.

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

In many cases, both Transfer 1 and 2 can be elided by the compiler - the object v is constructed directly in the location of a and no transferring is necessary. The compiler has to make use of Named Return Value Optimization for Transfer 1 in this example, because the object being returned is named. If we disable copy/move elision, however, each transfer involves a call to either A's copy constructor or its move constructor. In most modern compilers, the compiler will see that v is about to be destroyed and it will first move it into the return value. Then this temporary return value will be moved into a. If A does not have a move constructor, it will be copied for both transfers instead.

现在让我们看看:

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

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

The value we're returning comes from the reference being passed into the function. The compiler doesn't just assume that v is a temporary and that it's okay to move from it1. In this case, Transfer 1 will be a copy. Then Transfer 2 will be a move - that's okay because the returned value is still a temporary (we didn't return a reference). But since we know that we've taken an object that we can move from, because our parameter is an rvalue reference, we can explicitly tell the compiler to treat v as a temporary with std::move:

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

现在Transfer 1 和Transfer 2 都是移动.

Now both Transfer 1 and Transfer 2 will be moves.

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

1 The reason why the compiler doesn't automatically treat v, defined as A&&, as an rvalue is one of safety. It's not just too stupid to figure it out. Once an object has a name, it can be referred to multiple times throughout your code. Consider:

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

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

If a was automatically treated as an rvalue, doSomething would be free to rip its guts out, meaning that the a being passed to doSomethingElse may be invalid. Even if doSomething took its argument by value, the object would be moved from and therefore invalid in the next line. To avoid this problem, named rvalue references are lvalues. That means when doSomething is called, a will at worst be copied from, if not just taken by lvalue reference - it will still be valid in the next line.

compute 的作者可以说,好的,现在我允许从这个值中移动,因为我确定它是一个临时对象".你可以通过说 std::move(a) 来做到这一点.例如,您可以给 doSomething 一个副本,然后允许 doSomethingElse 从中移动:

It is up to the author of compute to say, "okay, now I allow this value to be moved from, because I know for certain that it's a temporary object". You do this by saying std::move(a). For example, you could give doSomething a copy and then allow doSomethingElse to move from it:

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

相关文章