17

一篇C++Next 博客文章

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

4 回答 4

25

So, lets say you have:

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

And you're doing:

A a = compute();

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.

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.

Now lets look at:

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

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);
}

Now both Transfer 1 and Transfer 2 will be moves.


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);
}

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.

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));
}
于 2012-11-17T13:37:29.097 回答
5

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

于 2012-11-17T13:15:31.020 回答
5

第一个利用了比移动更好的 NVRO。没有副本比便宜的更好。

The second cannot take advantage of NVRO. Assuming no elision, return temp; would call the copy constructor and return std::move(temp); would call the move constructor. Now, I believe either of these have equal potential to be elided and so you should go with the cheaper one if not elided, which is using std::move.

于 2012-11-17T13:20:38.947 回答
1

In C++17 and later

C++17 changed the definition of value categories so that the kind of copy elision you describe is guaranteed (see: How does guaranteed copy elision work?). Thus if you write your code in C++17 or later you should absolutely not return by std::move()'ing in this case.

于 2018-07-10T20:22:39.887 回答