C++ 什么是移动语义?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3106110/
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
What is move semantics?
提问by dicroce
I just finished listening to the Software Engineering radio podcast interview with Scott Meyersregarding C++0x. Most of the new features made sense to me, and I am actually excited about C++0x now, with the exception of one. I still don't get move semantics... What is it exactly?
我刚刚听完Scott Meyers对C++0x的软件工程广播播客采访。大多数新功能对我来说都很有意义,而且我现在对 C++0x 感到非常兴奋,除了一个。我仍然没有得到移动语义......它到底是什么?
回答by fredoverflow
I find it easiest to understand move semantics with example code. Let's start with a very simple string class which only holds a pointer to a heap-allocated block of memory:
我发现使用示例代码最容易理解移动语义。让我们从一个非常简单的字符串类开始,它只保存一个指向堆分配内存块的指针:
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}
Since we chose to manage the memory ourselves, we need to follow the rule of three. I am going to defer writing the assignment operator and only implement the destructor and the copy constructor for now:
既然选择了自己管理内存,就需要遵循三原则。我将推迟编写赋值运算符,现在只实现析构函数和复制构造函数:
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}
The copy constructor defines what it means to copy string objects. The parameter const string& that
binds to all expressions of type string which allows you to make copies in the following examples:
复制构造函数定义了复制字符串对象的含义。该参数const string& that
绑定到所有字符串类型的表达式,允许您在以下示例中进行复制:
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
Now comes the key insight into move semantics. Note that only in the first line where we copy x
is this deep copy really necessary, because we might want to inspect x
later and would be very surprised if x
had changed somehow. Did you notice how I just said x
three times (four times if you include this sentence) and meant the exact same objectevery time? We call expressions such as x
"lvalues".
现在是对移动语义的关键洞察。请注意,只有在我们复制的第一行中,x
这个深拷贝才是真正必要的,因为我们可能想x
稍后检查,如果x
以某种方式发生变化,我们会感到非常惊讶。你有没有注意到我刚刚说x
了三遍(如果你包括这句话的话是四遍)并且每次都指的是完全相同的对象?我们称这样的表达式为x
“左值”。
The arguments in lines 2 and 3 are not lvalues, but rvalues, because the underlying string objects have no names, so the client has no way to inspect them again at a later point in time.
rvalues denote temporary objects which are destroyed at the next semicolon (to be more precise: at the end of the full-expression that lexically contains the rvalue). This is important because during the initialization of b
and c
, we could do whatever we wanted with the source string, and the client couldn't tell a difference!
第 2 行和第 3 行中的参数不是左值,而是右值,因为底层字符串对象没有名称,因此客户端无法在以后的某个时间点再次检查它们。右值表示在下一个分号处销毁的临时对象(更准确地说:在词法上包含右值的完整表达式的末尾)。这很重要,因为在b
and的初始化过程中c
,我们可以对源字符串做任何我们想做的事情,而客户端无法区分!
C++0x introduces a new mechanism called "rvalue reference" which, among other things, allows us to detect rvalue arguments via function overloading. All we have to do is write a constructor with an rvalue reference parameter. Inside that constructor we can do anything we wantwith the source, as long as we leave it in somevalid state:
C++0x 引入了一种称为“右值引用”的新机制,除其他外,它允许我们通过函数重载来检测右值参数。我们所要做的就是编写一个带有右值引用参数的构造函数。在构造函数中,我们可以对源做任何我们想做的事情,只要我们让它处于某种有效状态:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}
What have we done here? Instead of deeply copying the heap data, we have just copied the pointer and then set the original pointer to null (to prevent 'delete[]' from source object's destructor from releasing our 'just stolen data'). In effect, we have "stolen" the data that originally belonged to the source string. Again, the key insight is that under no circumstance could the client detect that the source had been modified. Since we don't really do a copy here, we call this constructor a "move constructor". Its job is to move resources from one object to another instead of copying them.
我们在这里做了什么?我们没有深入复制堆数据,而是复制了指针,然后将原始指针设置为 null(以防止源对象的析构函数中的“delete[]”释放我们“刚刚被盗的数据”)。实际上,我们“窃取”了最初属于源字符串的数据。同样,关键的见解是在任何情况下客户端都无法检测到源已被修改。由于我们在这里并没有真正进行复制,因此我们称这个构造函数为“移动构造函数”。它的工作是将资源从一个对象移动到另一个对象,而不是复制它们。
Congratulations, you now understand the basics of move semantics! Let's continue by implementing the assignment operator. If you're unfamiliar with the copy and swap idiom, learn it and come back, because it's an awesome C++ idiom related to exception safety.
恭喜,您现在了解了移动语义的基础知识!让我们继续实现赋值运算符。如果您不熟悉copy and swap idiom,请学习它并回来,因为它是与异常安全相关的很棒的 C++ 惯用语。
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};
Huh, that's it? "Where's the rvalue reference?" you might ask. "We don't need it here!" is my answer :)
咦,就这样?“右值引用在哪里?” 你可能会问。“我们这里不需要!” 是我的答案:)
Note that we pass the parameter that
by value, so that
has to be initialized just like any other string object. Exactly how is that
going to be initialized? In the olden days of C++98, the answer would have been "by the copy constructor". In C++0x, the compiler chooses between the copy constructor and the move constructor based on whether the argument to the assignment operator is an lvalue or an rvalue.
请注意,我们通过that
value传递参数,因此that
必须像任何其他字符串对象一样初始化。究竟将如何that
初始化?在C++98的旧时代,答案应该是“通过复制构造函数”。在 C++0x 中,编译器根据赋值运算符的参数是左值还是右值在复制构造函数和移动构造函数之间进行选择。
So if you say a = b
, the copy constructorwill initialize that
(because the expression b
is an lvalue), and the assignment operator swaps the contents with a freshly created, deep copy. That is the very definition of the copy and swap idiom -- make a copy, swap the contents with the copy, and then get rid of the copy by leaving the scope. Nothing new here.
因此,如果您说a = b
,复制构造函数将初始化that
(因为表达式b
是左值),并且赋值运算符将内容与新创建的深层副本交换。这就是复制和交换习语的定义——制作一个副本,将内容与副本交换,然后通过离开作用域来摆脱副本。这里没有什么新鲜事。
But if you say a = x + y
, the move constructorwill initialize that
(because the expression x + y
is an rvalue), so there is no deep copy involved, only an efficient move.
that
is still an independent object from the argument, but its construction was trivial,
since the heap data didn't have to be copied, just moved. It wasn't necessary to copy it because x + y
is an rvalue, and again, it is okay to move from string objects denoted by rvalues.
但是如果你说a = x + y
,移动构造函数将初始化that
(因为表达式x + y
是一个右值),所以没有涉及深度复制,只有一个有效的移动。
that
仍然是参数中的一个独立对象,但它的构造是微不足道的,因为不必复制堆数据,只需移动即可。没有必要复制它,因为它x + y
是一个右值,同样,可以从右值表示的字符串对象中移动。
To summarize, the copy constructor makes a deep copy, because the source must remain untouched. The move constructor, on the other hand, can just copy the pointer and then set the pointer in the source to null. It is okay to "nullify" the source object in this manner, because the client has no way of inspecting the object again.
总而言之,复制构造函数进行深度复制,因为源必须保持不变。另一方面,移动构造函数可以只复制指针,然后将源中的指针设置为空。以这种方式“无效”源对象是可以的,因为客户端无法再次检查对象。
I hope this example got the main point across. There is a lot more to rvalue references and move semantics which I intentionally left out to keep it simple. If you want more details please see my supplementary answer.
我希望这个例子能说明要点。还有很多关于右值引用和移动语义的内容,我故意省略了这些内容以保持简单。如果您想了解更多详细信息,请参阅我的补充回答。
回答by fredoverflow
My first answer was an extremely simplified introduction to move semantics, and many details were left out on purpose to keep it simple. However, there is a lot more to move semantics, and I thought it was time for a second answer to fill the gaps. The first answer is already quite old, and it did not feel right to simply replace it with a completely different text. I think it still serves well as a first introduction. But if you want to dig deeper, read on :)
我的第一个答案是对移动语义的极其简化的介绍,为了保持简单,故意省略了许多细节。然而,移动语义还有很多,我认为是时候用第二个答案来填补空白了。第一个答案已经很旧了,简单地用完全不同的文本替换它感觉不对。我认为它仍然可以很好地作为第一个介绍。但如果你想深入挖掘,请继续阅读:)
Stephan T. Lavavej took the time to provide valuable feedback. Thank you very much, Stephan!
Stephan T. Lavavej 花时间提供了宝贵的反馈。非常感谢你,斯蒂芬!
Introduction
介绍
Move semantics allows an object, under certain conditions, to take ownership of some other object's external resources. This is important in two ways:
移动语义允许一个对象在某些条件下获得其他对象的外部资源的所有权。这在两个方面很重要:
Turning expensive copies into cheap moves. See my first answer for an example. Note that if an object does not manage at least one external resource (either directly, or indirectly through its member objects), move semantics will not offer any advantages over copy semantics. In that case, copying an object and moving an object means the exact same thing:
class cannot_benefit_from_move_semantics { int a; // moving an int means copying an int float b; // moving a float means copying a float double c; // moving a double means copying a double char d[64]; // moving a char array means copying a char array // ... };
Implementing safe "move-only" types; that is, types for which copying does not make sense, but moving does. Examples include locks, file handles, and smart pointers with unique ownership semantics. Note: This answer discusses
std::auto_ptr
, a deprecated C++98 standard library template, which was replaced bystd::unique_ptr
in C++11. Intermediate C++ programmers are probably at least somewhat familiar withstd::auto_ptr
, and because of the "move semantics" it displays, it seems like a good starting point for discussing move semantics in C++11. YMMV.
将昂贵的副本变成廉价的举动。有关示例,请参阅我的第一个答案。请注意,如果一个对象不管理至少一个外部资源(直接或间接通过其成员对象),则移动语义将不会比复制语义提供任何优势。在这种情况下,复制对象和移动对象意味着完全相同的事情:
class cannot_benefit_from_move_semantics { int a; // moving an int means copying an int float b; // moving a float means copying a float double c; // moving a double means copying a double char d[64]; // moving a char array means copying a char array // ... };
实现安全的“仅移动”类型;也就是说,复制没有意义的类型,但移动有意义。示例包括具有唯一所有权语义的锁、文件句柄和智能指针。注意:这个答案讨论了
std::auto_ptr
一个不推荐使用的 C++98 标准库模板,它std::unique_ptr
在 C++11 中被替换。中级 C++ 程序员可能至少有点熟悉std::auto_ptr
,并且由于它显示了“移动语义”,这似乎是讨论 C++11 中移动语义的一个很好的起点。天啊。
What is a move?
什么是动?
The C++98 standard library offers a smart pointer with unique ownership semantics called std::auto_ptr<T>
. In case you are unfamiliar with auto_ptr
, its purpose is to guarantee that a dynamically allocated object is always released, even in the face of exceptions:
C++98 标准库提供了一个具有独特所有权语义的智能指针,称为std::auto_ptr<T>
. 如果您不熟悉auto_ptr
,它的目的是保证动态分配的对象始终被释放,即使在遇到异常时:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
The unusual thing about auto_ptr
is its "copying" behavior:
不寻常的auto_ptr
是它的“复制”行为:
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Note how the initialization of b
with a
does notcopy the triangle, but instead transfers the ownership of the triangle from a
to b
. We also say "a
is moved intob
" or "the triangle is movedfrom a
tob
". This may sound confusing because the triangle itself always stays at the same place in memory.
注意如何初始化b
与a
不不复制三角形,而是转移三角形的从所有权a
到b
。我们也说“a
被移入b
”或者“三角形移动从a
到b
”。这听起来可能令人困惑,因为三角形本身总是停留在内存中的同一个位置。
To move an object means to transfer ownership of some resource it manages to another object.
移动对象意味着将其管理的某些资源的所有权转移到另一个对象。
The copy constructor of auto_ptr
probably looks something like this (somewhat simplified):
的复制构造函数auto_ptr
可能看起来像这样(有些简化):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
Dangerous and harmless moves
危险无害的动作
The dangerous thing about auto_ptr
is that what syntactically looks like a copy is actually a move. Trying to call a member function on a moved-from auto_ptr
will invoke undefined behavior, so you have to be very careful not to use an auto_ptr
after it has been moved from:
危险的auto_ptr
是,在语法上看起来像副本的东西实际上是一个移动。尝试在移动后调用成员函数auto_ptr
将调用未定义的行为,因此您必须非常小心,不要auto_ptr
在移动后使用:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
But auto_ptr
is not alwaysdangerous. Factory functions are a perfectly fine use case for auto_ptr
:
但auto_ptr
并不总是危险的。工厂函数是一个非常好的用例auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Note how both examples follow the same syntactic pattern:
请注意这两个示例如何遵循相同的句法模式:
auto_ptr<Shape> variable(expression);
double area = expression->area();
And yet, one of them invokes undefined behavior, whereas the other one does not. So what is the difference between the expressions a
and make_triangle()
? Aren't they both of the same type? Indeed they are, but they have different value categories.
然而,其中一个调用了未定义的行为,而另一个则没有。那么表达式a
和之间有什么区别make_triangle()
?他们俩不是一个类型的吗?确实如此,但它们具有不同的价值类别。
Value categories
价值类别
Obviously, there must be some profound difference between the expression a
which denotes an auto_ptr
variable, and the expression make_triangle()
which denotes the call of a function that returns an auto_ptr
by value, thus creating a fresh temporary auto_ptr
object every time it is called. a
is an example of an lvalue, whereas make_triangle()
is an example of an rvalue.
显然,a
表示auto_ptr
变量的表达式make_triangle()
和表示调用返回一个auto_ptr
by 值的函数的表达式之间肯定有一些深刻的区别,因此auto_ptr
每次调用时都会创建一个新的临时对象。a
是一个左值的例子,而make_triangle()
是一个右值的例子。
Moving from lvalues such as a
is dangerous, because we could later try to call a member function via a
, invoking undefined behavior. On the other hand, moving from rvalues such as make_triangle()
is perfectly safe, because after the copy constructor has done its job, we cannot use the temporary again. There is no expression that denotes said temporary; if we simply write make_triangle()
again, we get a differenttemporary. In fact, the moved-from temporary is already gone on the next line:
从左值移出诸如此类a
是危险的,因为我们稍后可能会尝试通过 调用成员函数a
,从而调用未定义的行为。另一方面,从诸如此类的右值移动make_triangle()
是完全安全的,因为在复制构造函数完成其工作后,我们不能再次使用临时对象。没有表示该临时的表达式;如果我们简单地make_triangle()
再次写入,我们会得到一个不同的临时值。事实上,移动的临时文件已经在下一行消失了:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Note that the letters l
and r
have a historic origin in the left-hand side and right-hand side of an assignment. This is no longer true in C++, because there are lvalues that cannot appear on the left-hand side of an assignment (like arrays or user-defined types without an assignment operator), and there are rvalues which can (all rvalues of class types with an assignment operator).
请注意,字母l
和r
在作业的左侧和右侧都有历史渊源。这在 C++ 中不再正确,因为有些左值不能出现在赋值的左侧(如数组或没有赋值运算符的用户定义类型),而有些右值可以(类类型的所有右值)带有赋值运算符)。
An rvalue of class type is an expression whose evaluation creates a temporary object. Under normal circumstances, no other expression inside the same scope denotes the same temporary object.
类类型的右值是一个表达式,其求值会创建一个临时对象。一般情况下,同一个作用域内没有其他表达式表示同一个临时对象。
Rvalue references
右值引用
We now understand that moving from lvalues is potentially dangerous, but moving from rvalues is harmless. If C++ had language support to distinguish lvalue arguments from rvalue arguments, we could either completely forbid moving from lvalues, or at least make moving from lvalues explicitat call site, so that we no longer move by accident.
我们现在知道从左值移动有潜在危险,但从右值移动是无害的。如果 C++ 有语言支持来区分左值参数和右值参数,我们可以完全禁止从左值移动,或者至少在调用点明确地从左值移动,这样我们就不会再意外移动了。
C++11's answer to this problem is rvalue references. An rvalue reference is a new kind of reference that only binds to rvalues, and the syntax is X&&
. The good old reference X&
is now known as an lvalue reference. (Note that X&&
is nota reference to a reference; there is no such thing in C++.)
C++11 对这个问题的回答是rvalue references。右值引用是一种新的引用,它只绑定到右值,语法是X&&
. 好的旧引用X&
现在称为左值引用。(请注意,X&&
是不向基准的基准;没有这样的东西在C ++中)。
If we throw const
into the mix, we already have four different kinds of references. What kinds of expressions of type X
can they bind to?
如果我们const
混合使用,我们已经有四种不同类型的引用。X
它们可以绑定到哪些类型的表达式?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
In practice, you can forget about const X&&
. Being restricted to read from rvalues is not very useful.
在实践中,您可以忘记const X&&
. 限制从右值读取并不是很有用。
An rvalue reference
X&&
is a new kind of reference that only binds to rvalues.
右值引用
X&&
是一种仅绑定到右值的新引用。
Implicit conversions
隐式转换
Rvalue references went through several versions. Since version 2.1, an rvalue reference X&&
also binds to all value categories of a different type Y
, provided there is an implicit conversion from Y
to X
. In that case, a temporary of type X
is created, and the rvalue reference is bound to that temporary:
右值引用经历了几个版本。从 2.1 版开始,右值引用X&&
也绑定到不同类型的所有值类别Y
,前提是存在从Y
到的隐式转换X
。在这种情况下,X
会创建一个临时类型,并将右值引用绑定到该临时:
void some_function(std::string&& r);
some_function("hello world");
In the above example, "hello world"
is an lvalue of type const char[12]
. Since there is an implicit conversion from const char[12]
through const char*
to std::string
, a temporary of type std::string
is created, and r
is bound to that temporary. This is one of the cases where the distinction between rvalues (expressions) and temporaries (objects) is a bit blurry.
在上面的例子中,"hello world"
是一个类型的左值const char[12]
。由于存在从const char[12]
throughconst char*
到的隐式转换std::string
,std::string
因此创建了一个临时类型,并r
绑定到该临时类型。这是右值(表达式)和临时对象(对象)之间的区别有点模糊的情况之一。
Move constructors
移动构造函数
A useful example of a function with an X&&
parameter is the move constructorX::X(X&& source)
. Its purpose is to transfer ownership of the managed resource from the source into the current object.
带有X&&
参数的函数的一个有用示例是移动构造函数X::X(X&& source)
。其目的是将托管资源的所有权从源转移到当前对象。
In C++11, std::auto_ptr<T>
has been replaced by std::unique_ptr<T>
which takes advantage of rvalue references. I will develop and discuss a simplified version of unique_ptr
. First, we encapsulate a raw pointer and overload the operators ->
and *
, so our class feels like a pointer:
在 C++11 中,std::auto_ptr<T>
已被std::unique_ptr<T>
which 利用右值引用所取代。我将开发和讨论unique_ptr
. 首先,我们封装了一个原始指针并重载了操作符->
and *
,所以我们的类感觉就像一个指针:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
The constructor takes ownership of the object, and the destructor deletes it:
构造函数获取对象的所有权,析构函数删除它:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Now comes the interesting part, the move constructor:
现在是有趣的部分,移动构造函数:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
This move constructor does exactly what the auto_ptr
copy constructor did, but it can only be supplied with rvalues:
这个移动构造函数与auto_ptr
复制构造函数完全一样,但它只能提供右值:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
The second line fails to compile, because a
is an lvalue, but the parameter unique_ptr&& source
can only be bound to rvalues. This is exactly what we wanted; dangerous moves should never be implicit. The third line compiles just fine, because make_triangle()
is an rvalue. The move constructor will transfer ownership from the temporary to c
. Again, this is exactly what we wanted.
第二行编译失败,因为a
是左值,但参数unique_ptr&& source
只能绑定到右值。这正是我们想要的;危险的举动不应该是含蓄的。第三行编译得很好,因为make_triangle()
是一个右值。移动构造函数将所有权从临时转移到c
。同样,这正是我们想要的。
The move constructor transfers ownership of a managed resource into the current object.
移动构造函数将托管资源的所有权转移到当前对象中。
Move assignment operators
移动赋值运算符
The last missing piece is the move assignment operator. Its job is to release the old resource and acquire the new resource from its argument:
最后缺少的部分是移动赋值运算符。它的工作是释放旧资源并从其参数中获取新资源:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Note how this implementation of the move assignment operator duplicates logic of both the destructor and the move constructor. Are you familiar with the copy-and-swap idiom? It can also be applied to move semantics as the move-and-swap idiom:
请注意移动赋值运算符的这种实现如何复制析构函数和移动构造函数的逻辑。你熟悉复制和交换习语吗?它也可以作为移动和交换习语应用于移动语义:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Now that source
is a variable of type unique_ptr
, it will be initialized by the move constructor; that is, the argument will be moved into the parameter. The argument is still required to be an rvalue, because the move constructor itself has an rvalue reference parameter. When control flow reaches the closing brace of operator=
, source
goes out of scope, releasing the old resource automatically.
现在这source
是一个 type 变量unique_ptr
,它将被移动构造函数初始化;也就是说,参数将被移动到参数中。参数仍然需要是一个右值,因为移动构造函数本身有一个右值引用参数。当控制流到达 的右大括号时operator=
,source
超出范围,自动释放旧资源。
The move assignment operator transfers ownership of a managed resource into the current object, releasing the old resource. The move-and-swap idiom simplifies the implementation.
移动赋值运算符将托管资源的所有权转移到当前对象中,从而释放旧资源。移动和交换习语简化了实现。
Moving from lvalues
从左值移动
Sometimes, we want to move from lvalues. That is, sometimes we want the compiler to treat an lvalue as if it were an rvalue, so it can invoke the move constructor, even though it could be potentially unsafe.
For this purpose, C++11 offers a standard library function template called std::move
inside the header <utility>
.
This name is a bit unfortunate, because std::move
simply casts an lvalue to an rvalue; it does notmove anything by itself. It merely enablesmoving. Maybe it should have been named std::cast_to_rvalue
or std::enable_move
, but we are stuck with the name by now.
有时,我们想从左值转移。也就是说,有时我们希望编译器将左值视为右值,因此它可以调用移动构造函数,即使它可能不安全。为此,C++11 提供了一个std::move
在 header 中调用的标准库函数模板<utility>
。这个名字有点不幸,因为std::move
只是将左值转换为右值;它本身不会移动任何东西。它只是使移动成为可能。也许它应该被命名为std::cast_to_rvalue
or std::enable_move
,但我们现在坚持使用这个名字。
Here is how you explicitly move from an lvalue:
以下是您从左值显式移动的方法:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Note that after the third line, a
no longer owns a triangle. That's okay, because by explicitlywriting std::move(a)
, we made our intentions clear: "Dear constructor, do whatever you want with a
in order to initialize c
; I don't care about a
anymore. Feel free to have your way with a
."
请注意,在第三行之后,a
不再拥有三角形。没关系,因为通过明确地编写std::move(a)
,我们明确了我们的意图:“亲爱的构造函数,a
为初始化做任何你想做的c
事情;我不再关心a
了。随意使用a
。”
std::move(some_lvalue)
casts an lvalue to an rvalue, thus enabling a subsequent move.
std::move(some_lvalue)
将左值转换为右值,从而启用后续移动。
Xvalues
X值
Note that even though std::move(a)
is an rvalue, its evaluation does notcreate a temporary object. This conundrum forced the committee to introduce a third value category. Something that can be bound to an rvalue reference, even though it is not an rvalue in the traditional sense, is called an xvalue(eXpiring value). The traditional rvalues were renamed to prvalues(Pure rvalues).
请注意,即使std::move(a)
是右值,它的计算也不会创建临时对象。这个难题迫使委员会引入第三个价值类别。可以绑定到右值引用的东西,即使它不是传统意义上的右值,也被称为xvalue(eXpiring 值)。传统的右值被重命名为纯右值(纯右值)。
Both prvalues and xvalues are rvalues. Xvalues and lvalues are both glvalues(Generalized lvalues). The relationships are easier to grasp with a diagram:
prvalues 和 xvalues 都是右值。Xvalues 和 lvalues 都是glvalues(广义左值)。用图表更容易掌握这些关系:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Note that only xvalues are really new; the rest is just due to renaming and grouping.
请注意,只有 xvalues 才是真正新的;其余的只是由于重命名和分组。
C++98 rvalues are known as prvalues in C++11. Mentally replace all occurrences of "rvalue" in the preceding paragraphs with "prvalue".
C++98 右值在 C++11 中称为纯右值。将前面段落中所有出现的“rvalue”替换为“prvalue”。
Moving out of functions
移出函数
So far, we have seen movement into local variables, and into function parameters. But moving is also possible in the opposite direction. If a function returns by value, some object at call site (probably a local variable or a temporary, but could be any kind of object) is initialized with the expression after the return
statement as an argument to the move constructor:
到目前为止,我们已经看到了局部变量和函数参数的移动。但是也可以朝相反的方向移动。如果函数按值返回,则调用站点上的某个对象(可能是局部变量或临时对象,但可以是任何类型的对象)将使用return
语句后的表达式作为移动构造函数的参数进行初始化:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Perhaps surprisingly, automatic objects (local variables that are not declared as static
) can also be implicitlymoved out of functions:
也许令人惊讶的是,自动对象(未声明为 的局部变量static
)也可以隐式移出函数:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
How come the move constructor accepts the lvalue result
as an argument? The scope of result
is about to end, and it will be destroyed during stack unwinding. Nobody could possibly complain afterward that result
had changed somehow; when control flow is back at the caller, result
does not exist anymore! For that reason, C++11 has a special rule that allows returning automatic objects from functions without having to write std::move
. In fact, you should neveruse std::move
to move automatic objects out of functions, as this inhibits the "named return value optimization" (NRVO).
为什么移动构造函数接受左值result
作为参数?的范围result
即将结束,将在堆栈展开时销毁。没有人会抱怨事后result
发生了某种变化;当控制流返回调用者时,result
不再存在!出于这个原因,C++11 有一个特殊的规则,允许从函数返回自动对象,而无需编写std::move
. 事实上,您永远不应该使用std::move
将自动对象移出函数,因为这会抑制“命名返回值优化”(NRVO)。
Never use
std::move
to move automatic objects out of functions.
切勿用于
std::move
将自动对象移出函数。
Note that in both factory functions, the return type is a value, not an rvalue reference. Rvalue references are still references, and as always, you should never return a reference to an automatic object; the caller would end up with a dangling reference if you tricked the compiler into accepting your code, like this:
请注意,在两个工厂函数中,返回类型都是值,而不是右值引用。右值引用仍然是引用,和往常一样,你永远不应该返回对自动对象的引用;如果你欺骗编译器接受你的代码,调用者最终会得到一个悬空引用,就像这样:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Never return automatic objects by rvalue reference. Moving is exclusively performed by the move constructor, not by
std::move
, and not by merely binding an rvalue to an rvalue reference.
永远不要通过右值引用返回自动对象。移动完全由移动构造函数执行,而不是由
std::move
,也不仅仅是将右值绑定到右值引用。
Moving into members
进入会员
Sooner or later, you are going to write code like this:
迟早你会写这样的代码:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
Basically, the compiler will complain that parameter
is an lvalue. If you look at its type, you see an rvalue reference, but an rvalue reference simply means "a reference that is bound to an rvalue"; it does notmean that the reference itself is an rvalue! Indeed, parameter
is just an ordinary variable with a name. You can use parameter
as often as you like inside the body of the constructor, and it always denotes the same object. Implicitly moving from it would be dangerous, hence the language forbids it.
基本上,编译器会抱怨这parameter
是一个左值。如果查看它的类型,您会看到一个右值引用,但右值引用仅表示“绑定到右值的引用”;它并不能意味着引用本身就是右值!确实,parameter
只是一个有名字的普通变量。您可以parameter
在构造函数的主体内随意使用,并且它始终表示相同的对象。隐式地移动它是危险的,因此语言禁止它。
A named rvalue reference is an lvalue, just like any other variable.
命名的右值引用是左值,就像任何其他变量一样。
The solution is to manually enable the move:
解决方法是手动启用移动:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
You could argue that parameter
is not used anymore after the initialization of member
. Why is there no special rule to silently insert std::move
just as with return values? Probably because it would be too much burden on the compiler implementors. For example, what if the constructor body was in another translation unit? By contrast, the return value rule simply has to check the symbol tables to determine whether or not the identifier after the return
keyword denotes an automatic object.
您可能会争辩说parameter
,在member
. 为什么没有std::move
像返回值一样静默插入的特殊规则?可能是因为这会给编译器实现者带来太多负担。例如,如果构造函数体在另一个翻译单元中怎么办?相比之下,返回值规则只需检查符号表以确定return
关键字后面的标识符是否表示自动对象。
You can also pass the parameter
by value. For move-only types like unique_ptr
, it seems there is no established idiom yet. Personally, I prefer to pass by value, as it causes less clutter in the interface.
您还可以传递parameter
by 值。对于像 那样的仅移动类型unique_ptr
,似乎还没有既定的习语。就个人而言,我更喜欢按值传递,因为它会减少界面中的混乱。
Special member functions
特殊成员函数
C++98 implicitly declares three special member functions on demand, that is, when they are needed somewhere: the copy constructor, the copy assignment operator, and the destructor.
C++98 根据需要隐式声明了三个特殊成员函数,也就是在某处需要它们时:复制构造函数、复制赋值运算符和析构函数。
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
Rvalue references went through several versions. Since version 3.0, C++11 declares two additional special member functions on demand: the move constructor and the move assignment operator. Note that neither VC10 nor VC11 conforms to version 3.0 yet, so you will have to implement them yourself.
右值引用经历了几个版本。从 3.0 版开始,C++11 按需声明了两个额外的特殊成员函数:移动构造函数和移动赋值运算符。请注意,VC10 和 VC11 都不符合 3.0 版,因此您必须自己实现它们。
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
These two new special member functions are only implicitly declared if none of the special member functions are declared manually. Also, if you declare your own move constructor or move assignment operator, neither the copy constructor nor the copy assignment operator will be declared implicitly.
如果没有手动声明任何特殊成员函数,则仅隐式声明这两个新的特殊成员函数。此外,如果您声明自己的移动构造函数或移动赋值运算符,则不会隐式声明复制构造函数和复制赋值运算符。
What do these rules mean in practice?
这些规则在实践中意味着什么?
If you write a class without unmanaged resources, there is no need to declare any of the five special member functions yourself, and you will get correct copy semantics and move semantics for free. Otherwise, you will have to implement the special member functions yourself. Of course, if your class does not benefit from move semantics, there is no need to implement the special move operations.
如果你编写一个没有非托管资源的类,就不需要自己声明五个特殊成员函数中的任何一个,你将免费获得正确的复制语义和移动语义。否则,您将不得不自己实现特殊的成员函数。当然,如果您的类没有从移动语义中受益,则无需实现特殊的移动操作。
Note that the copy assignment operator and the move assignment operator can be fused into a single, unified assignment operator, taking its argument by value:
请注意,复制赋值运算符和移动赋值运算符可以融合为一个统一的赋值运算符,按值获取其参数:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
This way, the number of special member functions to implement drops from five to four. There is a tradeoff between exception-safety and efficiency here, but I am not an expert on this issue.
这样,要实现的特殊成员函数的数量从五个减少到四个。这里需要在异常安全和效率之间进行权衡,但我不是这个问题的专家。
Forwarding references (previouslyknown as Universal references)
转发引用(以前称为通用引用)
Consider the following function template:
考虑以下函数模板:
template<typename T>
void foo(T&&);
You might expect T&&
to only bind to rvalues, because at first glance, it looks like an rvalue reference. As it turns out though, T&&
also binds to lvalues:
您可能希望T&&
只绑定到右值,因为乍一看,它看起来像一个右值引用。事实证明,它T&&
也绑定到左值:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
If the argument is an rvalue of type X
, T
is deduced to be X
, hence T&&
means X&&
. This is what anyone would expect.
But if the argument is an lvalue of type X
, due to a special rule, T
is deduced to be X&
, hence T&&
would mean something like X& &&
. But since C++ still has no notion of references to references, the type X& &&
is collapsedinto X&
. This may sound confusing and useless at first, but reference collapsing is essential for perfect forwarding(which will not be discussed here).
如果参数是类型的右值X
,T
则推导为X
,因此T&&
意味着X&&
。这是任何人都会期望的。但是如果参数是类型的左值X
,由于特殊规则,T
被推导出为X&
,因此T&&
将意味着类似X& &&
. 但是由于 C++ 仍然没有引用引用的概念,因此类型X& &&
被折叠为X&
. 乍一看,这可能听起来令人困惑和无用,但参考折叠对于完美转发至关重要(此处将不讨论)。
T&& is not an rvalue reference, but a forwarding reference. It also binds to lvalues, in which case
T
andT&&
are both lvalue references.
T&& 不是右值引用,而是转发引用。它还绑定到左值,在这种情况下
T
和T&&
都是左值引用。
If you want to constrain a function template to rvalues, you can combine SFINAEwith type traits:
如果要将函数模板限制为右值,可以将SFINAE与类型特征结合使用:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Implementation of move
移动的实现
Now that you understand reference collapsing, here is how std::move
is implemented:
现在您了解了参考折叠,以下std::move
是实现方式:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
As you can see, move
accepts any kind of parameter thanks to the forwarding reference T&&
, and it returns an rvalue reference. The std::remove_reference<T>::type
meta-function call is necessary because otherwise, for lvalues of type X
, the return type would be X& &&
, which would collapse into X&
. Since t
is always an lvalue (remember that a named rvalue reference is an lvalue), but we want to bind t
to an rvalue reference, we have to explicitly cast t
to the correct return type.
The call of a function that returns an rvalue reference is itself an xvalue. Now you know where xvalues come from ;)
如您所见,move
由于转发引用T&&
,它可以接受任何类型的参数,并返回一个右值引用。在std::remove_reference<T>::type
元函数调用是必要的,否则,类型的左值X
,返回类型是X& &&
,这将折叠成X&
。由于t
总是一个左值(记住命名的右值引用是一个左值),但是我们想要绑定t
到一个右值引用,我们必须显式地t
转换为正确的返回类型。返回右值引用的函数调用本身就是一个 xvalue。现在你知道 xvalues 来自哪里 ;)
The call of a function that returns an rvalue reference, such as
std::move
, is an xvalue.
返回右值引用(例如 )的函数调用
std::move
是 xvalue。
Note that returning by rvalue reference is fine in this example, because t
does not denote an automatic object, but instead an object that was passed in by the caller.
请注意,在此示例中通过右值引用返回是可以的,因为t
它不表示自动对象,而是由调用者传入的对象。
回答by sbi
Move semantics are based on rvalue references.
An rvalue is a temporary object, which is going to be destroyed at the end of the expression. In current C++, rvalues only bind to const
references. C++1x will allow non-const
rvalue references, spelled T&&
, which are references to an rvalue objects.
Since an rvalue is going to die at the end of an expression, you can steal its data. Instead of copyingit into another object, you moveits data into it.
移动语义基于右值引用。
右值是一个临时对象,它将在表达式结束时被销毁。在当前的 C++ 中,右值仅绑定到const
引用。C++1x 将允许非const
右值引用,拼写T&&
为对右值对象的引用。
由于右值将在表达式结束时死亡,因此您可以窃取其数据。不是将其复制到另一个对象中,而是将其数据移动到其中。
class X {
public:
X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
: data_()
{
// since 'x' is an rvalue object, we can steal its data
this->swap(std::move(rhs));
// this will leave rhs with the empty data
}
void swap(X&& rhs);
// ...
};
// ...
X f();
X x = f(); // f() returns result as rvalue, so this calls move-ctor
In the above code, with old compilers the result of f()
is copiedinto x
using X
's copy constructor. If your compiler supports move semantics and X
has a move-constructor, then that is called instead. Since its rhs
argument is an rvalue, we know it's not needed any longer and we can steal its value.
So the value is movedfrom the unnamed temporary returned from f()
to x
(while the data of x
, initialized to an empty X
, is moved into the temporary, which will get destroyed after the assignment).
在上面的代码中,旧编译器的结果f()
被复制到x
usingX
的复制构造函数中。如果您的编译器支持移动语义并X
具有移动构造函数,则改为调用它。因为它的rhs
参数是一个rvalue,我们知道它不再需要了,我们可以窃取它的价值。
因此,该值从从to返回的未命名临时移动(而 的数据,初始化为空,被移动到临时,分配后将被销毁)。f()
x
x
X
回答by Greg Hewgill
Suppose you have a function that returns a substantial object:
假设您有一个返回实体对象的函数:
Matrix multiply(const Matrix &a, const Matrix &b);
When you write code like this:
当你写这样的代码时:
Matrix r = multiply(a, b);
then an ordinary C++ compiler will create a temporary object for the result of multiply()
, call the copy constructor to initialise r
, and then destruct the temporary return value. Move semantics in C++0x allow the "move constructor" to be called to initialise r
by copying its contents, and then discard the temporary value without having to destruct it.
那么普通的C++编译器会为 的结果创建一个临时对象multiply()
,调用复制构造函数初始化r
,然后析构临时返回值。C++0x 中的移动语义允许调用“移动构造函数”r
通过复制其内容来初始化,然后丢弃临时值而不必破坏它。
This is especially important if (like perhaps the Matrix
example above), the object being copied allocates extra memory on the heap to store its internal representation. A copy constructor would have to either make a full copy of the internal representation, or use reference counting and copy-on-write semantics interally. A move constructor would leave the heap memory alone and just copy the pointer inside the Matrix
object.
如果(可能像Matrix
上面的例子一样),被复制的对象在堆上分配额外的内存来存储其内部表示,这一点尤其重要。复制构造函数必须要么制作内部表示的完整副本,要么在内部使用引用计数和写时复制语义。移动构造函数将单独保留堆内存并仅复制Matrix
对象内的指针。
回答by James McNellis
If you are really interested in a good, in-depth explanation of move semantics, I'd highly recommend reading the original paper on them, "A Proposal to Add Move Semantics Support to the C++ Language."
如果您真的对移动语义的良好、深入的解释感兴趣,我强烈建议您阅读有关它们的原始论文“向 C++ 语言添加移动语义支持的建议”。
It's very accessible and easy to read and it makes an excellent case for the benefits that they offer. There are other more recent and up to date papers about move semantics available on the WG21 website, but this one is probably the most straightforward since it approaches things from a top-level view and doesn't get very much into the gritty language details.
它非常易于访问且易于阅读,并且是它们提供的好处的绝佳案例。WG21 网站上还有其他关于移动语义的最新论文,但这一篇可能是最直接的,因为它从顶级视图处理事物,并且没有过多涉及粗暴的语言细节。
回答by Dave Abrahams
Move semanticsis about transferring resources rather than copying themwhen nobody needs the source value anymore.
移动语义是关于传输资源而不是在没有人需要源值时复制它们。
In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you're returning is copied to the caller's stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort
that just rearrange items, reallocation in vector
when its capacity()
is exceeded, etc.
在 C++03 中,对象经常被复制,只有在任何代码再次使用该值之前被销毁或赋值。例如,当你从一个函数中按值返回时——除非 RVO 启动——你返回的值被复制到调用者的堆栈帧,然后它超出范围并被销毁。这只是众多示例中的一个:当源对象是临时对象时查看按值传递,类似的算法sort
只是重新排列项目,超出vector
时重新分配capacity()
等。
When such copy/destroy pairs are expensive, it's typically because the object owns some heavyweight resource. For example, vector<string>
may own a dynamically-allocated memory block containing an array of string
objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Thenyou need deallocate all that memory you just copied. However, movinga large vector<string>
means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.
当这种复制/销毁对很昂贵时,通常是因为对象拥有一些重量级资源。例如,vector<string>
可能拥有一个动态分配的内存块,其中包含一个string
对象数组,每个对象都有自己的动态内存。复制这样的对象成本很高:您必须为源中的每个动态分配的块分配新内存,并复制所有值。 然后你需要释放你刚刚复制的所有内存。然而,移动一个大vector<string>
意味着只需将几个指针(指的是动态内存块)复制到目标并在源中将它们清零。
回答by Peregring-lk
In easy (practical) terms:
用简单(实用)的话来说:
Copying an object means copying its "static" members and calling the new
operator for its dynamic objects. Right?
复制对象意味着复制其“静态”成员并new
为其动态对象调用运算符。对?
class A
{
int i, *p;
public:
A(const A& a) : i(a.i), p(new int(*a.p)) {}
~A() { delete p; }
};
However, to movean object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.
然而,移动一个对象(我再说一遍,从实际的角度来看)意味着只复制动态对象的指针,而不是创建新对象。
But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should "invalidate" the source pointers to avoid destructing them twice:
但是,这不危险吗?当然,您可以两次破坏动态对象(分段错误)。因此,为了避免这种情况,您应该“无效”源指针以避免破坏它们两次:
class A
{
int i, *p;
public:
// Movement of an object inside a copy constructor.
A(const A& a) : i(a.i), p(a.p)
{
a.p = nullptr; // pointer invalidated.
}
~A() { delete p; }
// Deleting NULL, 0 or nullptr (address 0x0) is safe.
};
Ok, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that's very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, ..., you can call it with different names):
好的,但是如果我移动一个对象,源对象就变得没用了,不是吗?当然,但在某些情况下这非常有用。最明显的一个是当我用匿名对象(时间,右值对象,...,你可以用不同的名字调用它)调用一个函数时:
void heavyFunction(HeavyType());
In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don't need the anonymous object and you can save time and memory.
在这种情况下,会创建一个匿名对象,然后复制到函数参数,然后删除。所以,这里最好移动对象,因为不需要匿名对象,可以节省时间和内存。
This leads to the concept of an "rvalue" reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an "lvalue" is an assignable entity (the left part of the =
operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. So:
这导致了“右值”引用的概念。它们存在于 C++11 中只是为了检测接收到的对象是否是匿名的。我认为您已经知道“左值”是一个可赋值的实体(=
运算符的左侧部分),因此您需要一个对对象的命名引用才能充当左值。右值正好相反,一个没有命名引用的对象。因此,匿名对象和右值是同义词。所以:
class A
{
int i, *p;
public:
// Copy
A(const A& a) : i(a.i), p(new int(*a.p)) {}
// Movement (&& means "rvalue reference to")
A(A&& a) : i(a.i), p(a.p)
{
a.p = nullptr;
}
~A() { delete p; }
};
In this case, when an object of type A
should be "copied", the compiler creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.
在这种情况下,当一个类型的对象A
应该被“复制”时,编译器会根据传递的对象是否被命名来创建一个左值引用或一个右值引用。如果不是,则调用您的移动构造函数,并且您知道该对象是暂时的,您可以移动其动态对象而不是复制它们,从而节省空间和内存。
It is important to remember that "static" objects are always copied. There's no ways to "move" a static object (object in stack and not on heap). So, the distinction "move"/ "copy" when an object has no dynamic members (directly or indirectly) is irrelevant.
重要的是要记住“静态”对象总是被复制。没有办法“移动”静态对象(堆栈中的对象而不是堆中的对象)。因此,当对象没有动态成员(直接或间接)时,“移动”/“复制”的区别是无关紧要的。
If your object is complex and the destructor has other secondary effects, like calling to a library's function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:
如果您的对象很复杂并且析构函数具有其他次要效果,例如调用库函数、调用其他全局函数或其他任何函数,也许最好用标志来表示移动:
class Heavy
{
bool b_moved;
// staff
public:
A(const A& a) { /* definition */ }
A(A&& a) : // initialization list
{
a.b_moved = true;
}
~A() { if (!b_moved) /* destruct object */ }
};
So, your code is shorter (you don't need to do a nullptr
assignment for each dynamic member) and more general.
因此,您的代码更短(您不需要nullptr
为每个动态成员进行赋值)并且更通用。
Other typical question: what is the difference between A&&
and const A&&
? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can't modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.
其他典型问题:A&&
和之间有什么区别const A&&
?当然,在第一种情况下,您可以修改对象,而在第二种情况下不能,但是,实际意义吗?在第二种情况下,您无法修改它,因此您无法使对象无效(除非使用可变标志或类似的东西),并且复制构造函数没有实际区别。
And what is perfect forwarding? It is important to know that a "rvalue reference" is a reference to a named object in the "caller's scope". But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn't received like a temporal object.
什么是完美转发?重要的是要知道“右值引用”是对“调用者范围”中命名对象的引用。但在实际作用域中,右值引用是对象的名称,因此,它充当命名对象。如果将右值引用传递给另一个函数,则传递的是命名对象,因此,不会像时间对象那样接收该对象。
void some_function(A&& a)
{
other_function(a);
}
The object a
would be copied to the actual parameter of other_function
. If you want the object a
continues being treated as a temporary object, you should use the std::move
function:
该对象a
将被复制到 的实际参数other_function
。如果您希望对象a
继续被视为临时对象,则应使用该std::move
函数:
other_function(std::move(a));
With this line, std::move
will cast a
to an rvalue and other_function
will receive the object as a unnamed object. Of course, if other_function
has not specific overloading to work with unnamed objects, this distinction is not important.
使用这一行,std::move
将a
转换为右值并将other_function
对象作为未命名对象接收。当然,如果other_function
没有特定的重载来处理未命名的对象,这个区别并不重要。
Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:
那是完美转发吗?不是,但我们非常接近。完美转发仅对使用模板有用,目的是说:如果我需要将一个对象传递给另一个函数,我需要如果我收到一个命名对象,则该对象作为命名对象传递,否则,我想像一个未命名的对象一样传递它:
template<typename T>
void some_function(T&& a)
{
other_function(std::forward<T>(a));
}
That's the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward
. This function exploits some rules of template instantiation:
这是使用完美转发的原型函数的签名,在 C++11 中通过std::forward
. 该函数利用了模板实例化的一些规则:
`A& && == A&`
`A&& && == A&&`
So, if T
is a lvalue reference to A
(T= A&), a
also (A&&& => A&). If T
is a rvalue reference to A
, a
also (A&& && => A&&). In both cases, a
is a named object in the actual scope, but T
contains the information of its "reference type" from the caller scope's point of view. This information (T
) is passed as template parameter to forward
and 'a' is moved or not according to the type of T
.
因此,如果T
是对A
( T= A&)的左值引用,a
也是 ( A&&& => A& )。如果T
是对 的右值引用A
,则a
也是 (A&& && => A&&)。在这两种情况下,a
都是实际作用域中的命名对象,但从T
调用者作用域的角度来看,包含其“引用类型”的信息。此信息 ( T
) 作为模板参数传递给forward
并且 'a' 根据 的类型移动或不移动T
。
回答by Terry Mahaffey
It's like copy semantics, but instead of having to duplicate all of the data you get to steal the data from the object being "moved" from.
这就像复制语义,但您不必复制所有数据,而是从被“移动”的对象中窃取数据。
回答by snk_kid
You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the compiler generates them implicitly. This will do a copy.
你知道复制语义是什么意思吗?这意味着您具有可复制的类型,对于用户定义的类型,您定义它要么显式地编写复制构造函数和赋值运算符,要么编译器隐式生成它们。这将做一个副本。
Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it's source argument it 'moves' memory from the source to the destination.
移动语义基本上是带有构造函数的用户定义类型,它采用非 const 的 r 值引用(使用 &&(是的两个&符号)的新型引用),这称为移动构造函数,赋值运算符也是如此。那么移动构造函数是做什么的,而不是从它的源参数复制内存,而是将内存从源“移动”到目标。
When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:
你什么时候想这样做?好吧 std::vector 就是一个例子,假设你创建了一个临时的 std::vector 并且你从一个函数中返回它说:
std::vector<foo> get_foos();
You're going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it's pointers and 'move' dynamically allocated memory to the new instance. It's kind of like transfer-of-ownership semantics with std::auto_ptr.
当函数返回时,您将从复制构造函数中获得开销,如果(并且在 C++0x 中)std::vector 有一个移动构造函数而不是复制它可以只设置它的指针并动态分配“移动”内存到新实例。这有点像 std::auto_ptr 的所有权转移语义。
回答by Andreas DM
To illustrate the need for move semantics, let's consider this example without move semantics:
为了说明移动语义的必要性,让我们考虑这个没有移动语义的例子:
Here's a function that takes an object of type T
and returns an object of the same type T
:
这是一个函数,它接受一个类型T
的对象并返回一个相同类型的对象T
:
T f(T o) { return o; }
//^^^ new object constructed
The above function uses call by valuewhich means that when this function is called an object must be constructedto be used by the function.
Because the function also returns by value, another new object is constructed for the return value:
上面的函数使用按值调用,这意味着当这个函数被调用时,必须构造一个对象才能被该函数使用。
由于该函数也按值返回,因此为返回值构造了另一个新对象:
T b = f(a);
//^ new object constructed
Twonew objects have been constructed, one of which is a temporary object that's only used for the duration of the function.
已经构造了两个新对象,其中一个是仅在函数运行期间使用的临时对象。
When the new object is created from the return value, the copy constructor is called to copythe contents of the temporary object to the new object b. After the function completes, the temporary object used in the function goes out of scope and is destroyed.
当从返回值创建新对象时,调用复制构造函数将临时对象的内容复制到新对象 b 中。函数完成后,函数中使用的临时对象超出范围并被销毁。
Now, let's consider what a copy constructordoes.
现在,让我们考虑复制构造函数的作用。
It must first initialize the object, then copy all the relevant data from the old object to the new one.
Depending on the class, maybe its a container with very much data, then that could represent much timeand memory usage
它必须首先初始化对象,然后将所有相关数据从旧对象复制到新对象。
根据类的不同,它可能是一个包含大量数据的容器,那么这可能代表大量的时间和内存使用量
// Copy constructor
T::T(T &old) {
copy_data(m_a, old.m_a);
copy_data(m_b, old.m_b);
copy_data(m_c, old.m_c);
}
With move semanticsit's now possible to make most of this work less unpleasant by simply movingthe data rather than copying.
使用移动语义,现在可以通过简单地移动数据而不是复制来减少大部分工作的不愉快。
// Move constructor
T::T(T &&old) noexcept {
m_a = std::move(old.m_a);
m_b = std::move(old.m_b);
m_c = std::move(old.m_c);
}
Moving the data involves re-associating the data with the new object. And no copy takes placeat all.
移动数据涉及将数据与新对象重新关联。并且根本没有复制发生。
This is accomplished with an rvalue
reference.
An rvalue
reference works pretty much like an lvalue
reference with one important difference:
an rvalue reference can be movedand an lvaluecannot.
这是通过rvalue
引用完成的。
一个rvalue
参考的工作非常像一个lvalue
有一个重要区别参考:
一个右值引用可以移动和左值不能。
From cppreference.com:
To make strong exception guarantee possible, user-defined move constructors should not throw exceptions. In fact, standard containers typically rely on std::move_if_noexcept to choose between move and copy when container elements need to be relocated. If both copy and move constructors are provided, overload resolution selects the move constructor if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy constructor if the argument is an lvalue (named object or a function/operator returning lvalue reference). If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable. In many situations, move constructors are optimized out even if they would produce observable side-effects, see copy elision. A constructor is called a 'move constructor' when it takes an rvalue reference as a parameter. It is not obligated to move anything, the class is not required to have a resource to be moved and a 'move constructor' may not be able to move a resource as in the allowable (but maybe not sensible) case where the parameter is a const rvalue reference (const T&&).
为了使强异常保证成为可能,用户定义的移动构造函数不应抛出异常。事实上,当容器元素需要重新定位时,标准容器通常依赖 std::move_if_noexcept 在移动和复制之间进行选择。如果同时提供了复制和移动构造函数,则重载决议在参数是右值(右值,如无名临时值或 xvalue,如 std::move 的结果)时选择移动构造函数,并在以下情况下选择复制构造函数参数是一个左值(命名对象或返回左值引用的函数/运算符)。如果只提供了复制构造函数,则所有参数类别都会选择它(只要它引用对 const 的引用,因为右值可以绑定到 const 引用),这使得在移动不可用时复制回退以进行移动。在许多情况下,即使移动构造函数会产生可观察到的副作用,也会对其进行优化,请参阅复制省略。当构造函数将右值引用作为参数时,它被称为“移动构造函数”。没有义务移动任何东西,类不需要移动资源,并且“移动构造函数”可能无法像在允许(但可能不合理)的情况下移动资源,其中参数是const 右值引用(const T&&)。