C++11 右值和移动语义混淆(返回语句)
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/4986673/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
C++11 rvalues and move semantics confusion (return statement)
提问by Tarantula
I'm trying to understand rvalue references and move semantics of C++11.
我正在尝试理解 C++11 的右值引用和移动语义。
What is the difference between these examples, and which of them is going to do no vector copy?
这些示例之间有什么区别,其中哪些将不进行矢量复制?
First example
第一个例子
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
Second example
第二个例子
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Third example
第三个例子
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
回答by Howard Hinnant
First example
第一个例子
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
The first example returns a temporary which is caught by rval_ref
. That temporary will have its life extended beyond the rval_ref
definition and you can use it as if you had caught it by value. This is very similar to the following:
第一个示例返回一个由rval_ref
. 该临时文件的寿命将超出rval_ref
定义范围,您可以像按值捕获它一样使用它。这与以下内容非常相似:
const std::vector<int>& rval_ref = return_vector();
except that in my rewrite you obviously can't use rval_ref
in a non-const manner.
除了在我的重写中,您显然不能rval_ref
以非常量的方式使用。
Second example
第二个例子
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
In the second example you have created a run time error. rval_ref
now holds a reference to the destructed tmp
inside the function. With any luck, this code would immediately crash.
在第二个示例中,您创建了一个运行时错误。 rval_ref
现在持有对函数tmp
内部的destructed 的引用。运气好的话,这段代码会立即崩溃。
Third example
第三个例子
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Your third example is roughly equivalent to your first. The std::move
on tmp
is unnecessary and can actually be a performance pessimization as it will inhibit return value optimization.
您的第三个示例大致相当于您的第一个示例。该std::move
上tmp
是不必要的,而且实际上是一个性能pessimization,因为它会抑制返回值优化。
The best way to code what you're doing is:
对您正在做的事情进行编码的最佳方法是:
Best practice
最佳实践
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> rval_ref = return_vector();
I.e. just as you would in C++03. tmp
is implicitly treated as an rvalue in the return statement. It will either be returned via return-value-optimization (no copy, no move), or if the compiler decides it can not perform RVO, then it will use vector's move constructor to do the return. Only if RVO is not performed, and if the returned type did not have a move constructor would the copy constructor be used for the return.
即就像在 C++03 中一样。 tmp
在 return 语句中被隐式视为右值。它将通过返回值优化(无复制,无移动)返回,或者如果编译器决定它不能执行 RVO,那么它将使用 vector 的移动构造函数来执行 return。仅当未执行 RVO 并且返回的类型没有移动构造函数时,才会使用复制构造函数进行返回。
回答by Puppy
None of them will copy, but the second will refer to a destroyed vector. Named rvalue references almost never exist in regular code. You write it just how you would have written a copy in C++03.
它们都不会复制,但第二个将引用被破坏的向量。命名的右值引用几乎从不存在于常规代码中。您可以按照在 C++03 中编写副本的方式编写它。
std::vector<int> return_vector()
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> rval_ref = return_vector();
Except now, the vector is moved. The userof a class doesn't deal with it's rvalue references in the vast majority of cases.
除了现在,向量被移动。在绝大多数情况下,类的用户不会处理它的右值引用。
回答by Zoner
The simple answer is you should write code for rvalue references like you would regular references code, and you should treat them the same mentally 99% of the time. This includes all the old rules about returning references (i.e. never return a reference to a local variable).
简单的答案是,您应该像编写常规引用代码一样为右值引用编写代码,并且应该在 99% 的时间里在精神上对待它们。这包括所有关于返回引用的旧规则(即永远不要返回对局部变量的引用)。
Unless you are writing a template container class that needs to take advantage of std::forward and be able to write a generic function that takes either lvalue or rvalue references, this is more or less true.
除非您正在编写需要利用 std::forward 并能够编写接受左值或右值引用的通用函数的模板容器类,否则这或多或少是正确的。
One of the big advantages to the move constructor and move assignment is that if you define them, the compiler can use them in cases were the RVO (return value optimization) and NRVO (named return value optimization) fail to be invoked. This is pretty huge for returning expensive objects like containers & strings by value efficiently from methods.
移动构造函数和移动赋值的一大优点是,如果您定义它们,编译器可以在 RVO(返回值优化)和 NRVO(命名返回值优化)无法调用的情况下使用它们。这对于从方法有效地按值返回昂贵的对象(如容器和字符串)来说非常重要。
Now where things get interesting with rvalue references, is that you can also use them as arguments to normal functions. This allows you to write containers that have overloads for both const reference (const foo& other) and rvalue reference (foo&& other). Even if the argument is too unwieldy to pass with a mere constructor call it can still be done:
现在,右值引用的有趣之处在于,您还可以将它们用作普通函数的参数。这允许您编写具有常量引用(const foo& other)和右值引用(foo&& other)重载的容器。即使参数太笨重而无法仅通过构造函数调用传递,它仍然可以完成:
std::vector vec;
for(int x=0; x<10; ++x)
{
// automatically uses rvalue reference constructor if available
// because MyCheapType is an unamed temporary variable
vec.push_back(MyCheapType(0.f));
}
std::vector vec;
for(int x=0; x<10; ++x)
{
MyExpensiveType temp(1.0, 3.0);
temp.initSomeOtherFields(malloc(5000));
// old way, passed via const reference, expensive copy
vec.push_back(temp);
// new way, passed via rvalue reference, cheap move
// just don't use temp again, not difficult in a loop like this though . . .
vec.push_back(std::move(temp));
}
The STL containers have been updated to have move overloads for nearly anything (hash key and values, vector insertion, etc), and is where you will see them the most.
STL 容器已更新为几乎所有内容(哈希键和值、向量插入等)都具有移动重载,并且是您最常看到它们的地方。
You can also use them to normal functions, and if you only provide an rvalue reference argument you can force the caller to create the object and let the function do the move. This is more of an example than a really good use, but in my rendering library, I have assigned a string to all the loaded resources, so that it is easier to see what each object represents in the debugger. The interface is something like this:
您也可以将它们用于普通函数,如果您只提供一个右值引用参数,您可以强制调用者创建对象并让函数执行移动。这与其说是一个很好的用途,不如说是一个示例,但在我的渲染库中,我为所有加载的资源分配了一个字符串,以便更容易查看每个对象在调试器中代表什么。界面是这样的:
TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
tex->friendlyName = std::move(friendlyName);
return tex;
}
It is a form of a 'leaky abstraction' but allows me to take advantage of the fact I had to create the string already most of the time, and avoid making yet another copying of it. This isn't exactly high-performance code but is a good example of the possibilities as people get the hang of this feature. This code actually requires that the variable either be a temporary to the call, or std::move invoked:
这是一种“泄漏抽象”的形式,但允许我利用我在大多数情况下必须创建字符串的事实,并避免再次复制它。这不是完全高性能的代码,但是当人们掌握了这个功能时,它是一个很好的例子。这段代码实际上要求变量要么是调用的临时变量,要么是调用 std::move:
// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));
or
或者
// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));
or
或者
// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));
but this won't compile!
但这不会编译!
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
回答by Red XIII
Not an answer per se, but a guideline. Most of the time there is not much sense in declaring local T&&
variable (as you did with std::vector<int>&& rval_ref
). You will still have to std::move()
them to use in foo(T&&)
type methods. There is also the problem that was already mentioned that when you try to return such rval_ref
from function you will get the standard reference-to-destroyed-temporary-fiasco.
本身不是答案,而是指南。大多数情况下,声明局部T&&
变量并没有多大意义(就像您对 所做的那样std::vector<int>&& rval_ref
)。您仍然需要std::move()
在foo(T&&)
类型方法中使用它们。还有一个已经提到的问题,当您尝试rval_ref
从函数返回此类时,您将获得标准的销毁临时惨败参考。
Most of the time I would go with following pattern:
大多数时候我会采用以下模式:
// Declarations
A a(B&&, C&&);
B b();
C c();
auto ret = a(b(), c());
You don't hold any refs to returned temporary objects, thus you avoid (inexperienced) programmer's error who wish to use a moved object.
您没有对返回的临时对象持有任何引用,因此您可以避免希望使用移动对象的(没有经验的)程序员的错误。
auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));
// Either these just fail (assert/exception), or you won't get
// your expected results due to their clean state.
bRet.foo();
cRet.bar();
Obviously there are (although rather rare) cases where a function truly returns a T&&
which is a reference to a non-temporaryobject that you can move into your object.
显然,在某些情况下(尽管相当少见),函数真正返回 a T&&
,它是对可以移入对象的非临时对象的引用。
Regarding RVO: these mechanisms generally work and compiler can nicely avoid copying, but in cases where the return path is not obvious (exceptions, if
conditionals determining the named object you will return, and probably couple others) rrefs are your saviors (even if potentially more expensive).
关于 RVO:这些机制通常有效,编译器可以很好地避免复制,但在返回路径不明显的情况if
下(例外,条件决定你将返回的命名对象,可能还有其他对象)rrefs 是你的救星(即使可能更多昂贵的)。
回答by Edward Strange
None of those will do any extra copying. Even if RVO isn't used, the new standard says that move construction is preferred to copy when doing returns I believe.
这些都不会做任何额外的复制。即使不使用 RVO,新标准也表明,在我认为返回时,移动构造优先于复制。
I do believe that your second example causes undefined behavior though because you're returning a reference to a local variable.
我确实相信您的第二个示例会导致未定义的行为,因为您正在返回对局部变量的引用。
回答by Andrej Podzimek
As already mentioned in comments to the first answer, the return std::move(...);
construct can make a difference in cases other than returning of local variables. Here's a runnable example that documents what happens when you return a member object with and without std::move()
:
正如在对第一个答案的评论中已经提到的那样,return std::move(...);
除了返回局部变量之外,该构造还可以在其他情况下产生影响。这是一个可运行的示例,它记录了当您返回带有和不带有 的成员对象时会发生什么std::move()
:
#include <iostream>
#include <utility>
struct A {
A() = default;
A(const A&) { std::cout << "A copied\n"; }
A(A&&) { std::cout << "A moved\n"; }
};
class B {
A a;
public:
operator A() const & { std::cout << "B C-value: "; return a; }
operator A() & { std::cout << "B L-value: "; return a; }
operator A() && { std::cout << "B R-value: "; return a; }
};
class C {
A a;
public:
operator A() const & { std::cout << "C C-value: "; return std::move(a); }
operator A() & { std::cout << "C L-value: "; return std::move(a); }
operator A() && { std::cout << "C R-value: "; return std::move(a); }
};
int main() {
// Non-constant L-values
B b;
C c;
A{b}; // B L-value: A copied
A{c}; // C L-value: A moved
// R-values
A{B{}}; // B R-value: A copied
A{C{}}; // C R-value: A moved
// Constant L-values
const B bc;
const C cc;
A{bc}; // B C-value: A copied
A{cc}; // C C-value: A copied
return 0;
}
Presumably, return std::move(some_member);
only makes sense if you actually want to move the particular class member, e.g. in a case where class C
represents short-lived adapter objects with the sole purpose of creating instances of struct A
.
据推测,return std::move(some_member);
只有当您确实想要移动特定的类成员时才有意义,例如在class C
表示短期适配器对象的唯一目的是创建struct A
.
Notice how struct A
always gets copiedout of class B
, even when the class B
object is an R-value. This is because the compiler has no way to tell that class B
's instance of struct A
won't be used any more. In class C
, the compiler does have this information from std::move()
, which is why struct A
gets moved, unless the instance of class C
is constant.
注意struct A
总是从 中复制出来class B
,即使class B
对象是 R 值。这是因为编译器无法判断将不再使用class B
的实例struct A
。在 中class C
,编译器确实有来自 的信息std::move()
,这就是为什么struct A
会被移动,除非 的实例class C
是常量。