移动语义

移动语义

移动语义(Move Semantics)应该是C++11标准带来的最大改进。通过移动语义,我们可以实现更为细致的内存管理。比如,从一个之后不再使用的对象复制数据时,我们可以通过移动语义手动回收这个对象可以被我们直接利用的内存数据,避免大规模的内存复制操作。对于移动操作的时间复杂度是常数时间的情况(vector,map,unordered_map,string等标准库对象都是这样),我们的性能收益是巨大的。那么,问题是我们如何知道一个对象在之后不再使用?这就需要了解右值(rvalue)的概念,它表示一个临时的,在之后不能被访问的值。对于下面的代码,user是一个左值(lvalue),字符串字面量"Skywalker"是一个右值(rvalue)。我们可以在这行代码运行之后访问user变量,但却不能访问到原始的字符串字面量"Skywalker"(读者思考下为什么?)。

[图片上传失败...(image-b9b24c-1614136368875)]


image.png

下图通过一个生活中的场景来说明移动语义(Move Semantics)。图1中,蓝色小人通过复制构造复制(重新生成或初始化,生成和初始化需要耗费大量时间)了红色小人房子里的家具,图2中,蓝色小人通过移动构造继承了红色小人房子里的家具。显然,对于蓝色小人来说,继承红色小人的家具比自己购买新的一模一样的家具的代价要小(不用花钱_),换成代码来说,就是继承比复制更高效。

[图片上传失败...(image-949699-1614136326416)]

[图片上传失败...(image-f2030e-1614136326416)]

下面给出了复制构造和移动构造的一个示例代码,当要复制的对象是一个右值(rvalue),会调用移动构造函数,其它情况调用复制构造函数。可以看出,一个典型的复制构造函数和典型的移动构造函数之间的不同:移动构造函数的参数没有使用const关键字,参数的变量名前有两个&符号,用来表示右值引用。因为rhs是右值引用,我们可以认为在之后它不会再被使用,所以移动构造函数直接复制了对象数据的内存指针,没有进行内存分配和数据的深copy。除了移动构造函数,我们还可以在赋值运算上这样使用移动语义。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n9" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background: rgb(51, 51, 51); position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">// Copy constructor
string(const string& rhs) {
allocateSpace(myDataPtr);
deepcopy(rhs.myDataPtr, myDataPtr);
}
// Move constructor
string(string&& rhs) {
myDataPtr = rhs.myDataPtr;
rhs.myDataPtr = nullptr;
}</pre>

对于一个可引用的变量(lvalue),如果我们确定之后不会再使用它,可以使用std::move手动将其变为右值参数。在之后的章节,我们还会讨论只能进行移动操作不能进行复制操作的对象。对于被移动的对象,如果没有重新初始化,我们不应该使用它。下面的代码演示了这一个过程,标准库中的unique_ptr类生成的对象就是一个只能移动,不能复制的对象。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n12" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background: rgb(51, 51, 51); position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">MoveOnlyObject a;
...
MoveOnlyObject b(a); // ERROR – copy constructor doesn't exist
MoveOnlyObject c(std::move(a)); // OK – ownership transferred to c. a is DEAD now
cout << *a; // RUNTIME ERROR – illegal access</pre>

上面的代码在a对象已经被移动后仍然访问了它,这样做的后果是不可预料的。我们可以向下面的代码这样,通过作用域来避免这种情况:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n14" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background: rgb(51, 51, 51); position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">MoveOnlyObject c;
{
MoveOnlyObject a;
...
c = MoveOnlyObject(std::move(a));
} // can't even attempt to dereference a anymore</pre>

另一个需要牢记的地方,在向容器插入对象时,如果临时变量以后不再使用,应该通过std::move将其转为右值参数,避免不必要的内存数据复制:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n16" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background: rgb(51, 51, 51); position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">vector<string> importantUsers;
...
string localUser;
... // compute local user
allUsers.push_back(std::move(localUser));</pre>

关于移动语义,还有许多细节我们没有提到:比如,&&符号在其它情形下的使用等等。另外,需要l注意的是位于栈上的内存不能被移动操作复用,也就是说如果一个类只包含编译器自动维护作用域的变量(类中的变量都实际在栈中占用了连续的内存块,而不是通过类似指针的方式引用,读者认真思考为什么?可以考虑string对象因为会使用指针引用动态申请的内存,所以它不是,可以从移动语义收益,而只包含char str[50]的类,不能从移动语义收益),移动操作对其不会有任何提升。尽管不能收益于移动语义,我们仍可能为这样的类添加移动构造函数,来使操作它们的代码和其它类的代码看上去是一致的,但实际上,编译器会为这样的类自动生成移动构造,我们自己添加实际上是多此一举。对于包含这样的类的对象的标准库容器,比如vector等,由于容器的数据存储空间是动态申请的,并非来自栈上,我们仍能从移动语义中收益(移动操作复制的是存储空间的内存地址,而不是实际的每个类对象的实际数据)。基于此,对于使用标准库容器(比如vector)的程序来说,可以认为升级到C++11会自动获得一定的性能提升。

note

  • 认真思考移动语义是如何影响代码的性能表现。

  • 使用std::move()传递数据的所有权给容器。

  • 推荐将需要移动的对象定义在一个作用域中,并且在作用域的最后一行代码移动对象,从而避免再次使用已经被移动的对象。

右值特点:

不可修改

没名字

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容