一键式学习C ++ r值,&&和Move

Sean McAuliffeUnsplash拍摄的照片
C ++很难,新版本变得更难。 本文将介绍C ++中的一些硬性部分,右值,右值引用( && )和移动语义。 我将对这些复杂且相关的主题进行逆向工程(而不是隐喻),以便您可以一口气完全理解它们。

首先,让我们来看一下

什么是右值?

r值应位于等号的右侧。

例:

int var ; // too much JavaScript recently:)
var = 8; // OK! lvalue (yes, there is a lvalue) on the left

8 = var ; // ERROR! rvalue on the left
( var + 1) = 8; // ERROR! rvalue on the left

很简单。 接下来,让我们看一个更微妙的情况,即函数返回的r值:

#include <string>
#include <stdio.h>

int g_var = 8;
int& returnALvalue() {
return g_var; //here we return a lvalue
}

int returnARvalue() {
return g_var; //here we return a rvalue
}

int main() {
printf("%d", returnALvalue()++); // g_var += 1;
printf("%d", returnARvalue());
}

结果:

8
9

值得注意的是,返回左值的方法 (在示例中)被认为是不好的做法。 所以不要在现实世界中编程

超越理论水平

变量是否为r值甚至可以在发明&&之前就在实际编程中产生差异。

例如,这行

const int& var = 8;

可以在以下情况下编译良好:

int& var = 8; // use a lvalue reference for a rvalue

产生以下错误:

rvalue.cc:24:6: error: non-const lvalue reference to type 'int' cannot bind to a
temporary of type 'int'

该错误消息表示编译器对r值强制执行const引用

一个更有趣的示例:

#include <stdio.h>
#include <string>

void print( const std::string& name) {
printf("rvalue detected:%s\n", name.c_str());
}
void print(std::string& name) {
printf("lvalue detected:%s\n", name.c_str());
}
int main() {
std::string name = "lvalue";
print(name); //compiler can detect the right function for lvalue
print(rvalu + "e"); // likewise for rvalue
}

结果:

lvalue detected:lvalue
rvalue detected:rvalue

这种差异实际上足够大,编译器可以确定重载函数。

那么右值是常数吗?

不完全是。 这是&&r值参考)出现的地方。

例:

#include <stdio.h>
#include <string>
void print(const std::string& name) {
printf(“const value detected:%s\n”, name.c_str());
}
void print(std::string& name) {
printf(“lvalue detected%s\n”, name.c_str());
}
void print(std::string&& name) {
printf(“rvalue detected:%s\n”, name.c_str());
}
rvalu + "e"

结果:

lvalue detected:lvalue
const value detected:cvalue
rvalue detected:rvalue

如果函数重载了r值,则r值变量将选择一个更指定的版本,而不是该版本采用const引用参数,而这两个参数都兼容。 因此, && 可以使const值进一步分散rvalue。

在下面,我总结了默认设置中重载函数版本与不同类型的兼容性。 您可以通过在上面的示例中有选择地注释掉行来验证结果。

进一步区分r值和常数值确实很酷,因为它们实际上并不完全相同。 但是,实际价值是什么?

&&到底能解决什么问题?

问题是当参数为r值时,不必要的深度复制。

更具体。 提供&&表示法来指定r值,当r值1)作为构造函数赋值运算符的参数传递,以及2)其类包含指针时,可用于避免深度复制(或指针)引用动态分配的资源(内存)。

通过示例可以更具体

#include <stdio.h>
#include <string>
#include <algorithm>

using namespace std;

class ResourceOwner {
public:
ResourceOwner( const char res[]) {
theResource = new string(res);
}
ResourceOwner( const ResourceOwner& other) {
printf("copy %s\n", other.theResource->c_str());
theResource = new string(other.theResource->c_str());
}
ResourceOwner& operator=( const ResourceOwner& other) {
ResourceOwner tmp(other);
swap(theResource, tmp.theResource);
printf("assign %s\n", other.theResource->c_str());
}
~ResourceOwner() {
if (theResource) {
printf("destructor %s\n", theResource->c_str());
delete theResource;
}
}

private:
string* theResource;
};
void testCopy() { // case 1
printf("=====start testCopy()=====\n");
ResourceOwner res1("res1");
ResourceOwner res2 = res1; //copy res1
printf("=====destructors for stack vars, ignore=====\n");
}
void testAssign() { // case 2
printf("=====start testAssign()=====\n");
ResourceOwner res1("res1");
ResourceOwner res2("res2");
res2 = res1; //copy res1, assign res1, destrctor res2
printf("=====destructors for stack vars, ignore=====\n");
}
void testRValue() {  // case 3 
printf("=====start testRValue ()=====\n");

ResourceOwner res2("res2");
res2 = ResourceOwner("res1"); //copy res1, assign res1, destructor res2, destructor res1
printf("=====destructors for stack vars, ignore=====\n");
 int main() {
testCopy();
testAssign();
testRValue();
}

结果:

=====start testCopy()=====
copy res1
=====destructors for stack vars, ignore=====
destructor res1
destructor res1
=====start testAssign()=====
copy res1
assign res1
destructor res2
=====destructors for stack vars, ignore=====
destructor res1
destructor res1
=====start testRValue()=====
copy res1
assign res1
destructor res2
destructor res1
=====destructors for stack vars, ignore=====
destructor res1

结果都是不错的前两个测试案例,即testCopy()testAssign()其中,在资源 res1被复制的res2 。 复制资源是合理的,因为它们是两个实体,都需要它们的非共享资源(字符串)。

但是,在第三种情况下,在res1中进行资源的(深度)复制是多余的,因为匿名r值(由ResourceOwner(“res1”) )将在分配后立即被销毁,因此不再需要该资源:

destructor res1

我认为这是重复问题陈述的好机会:

提供&&表示法来指定r值,当r值1)作为构造函数或赋值运算符的参数传递时,以及2)其类包含指针时,可用于避免深度复制(或指针)引用动态分配的资源(内存)。

如果复制将要消失的资源不是最佳选择,那么正确的操作是什么? 答案是

移动

这个想法非常简单,如果参数为r值,则无需复制 。 相反,我们可以简单地“移动”资源(即r值指向的内存)。 现在,让我们使用新技术来重载赋值运算符

ResourceOwner& operator=(ResourceOwner&& other) {
theResource = other.theResource;
other.theResource = NULL;
}

这个新的赋值运算符称为移动 赋值运算符移动 构造器可以用类似的方式编程。

理解这一点的一个好方法是:当您出售旧资产并搬到新房时,您不必像情况3那样扔掉所有家具,对吗? 相反,您可以简单地家具移到新家中。

都好。

什么是std :: move?

除了上面讨论的move 赋值运算符move 构造函数外 ,此难题中还有最后一个缺少的部分std::move

再次,我们首先看问题:

当1)我们知道变量实际上是r值时,而2)编译器则不是。 无法调用正确版本的重载函数。

一个常见的情况是,当我们添加资源所有者的另一层ResourceHolder ,这三个实体的关系如下所示:

holder
|
|----->owner
|
|----->resource

(Nb,在下面的示例中,我也完成了ResourceOwnermove 构造函数的实现)

例:

#include <string>
#include <algorithm>
using namespace std;
class ResourceOwner {
public:
ResourceOwner(const char res[]) {
theResource = new string(res);
}
  ResourceOwner(const ResourceOwner& other) {
printf(“copy %s\n”, other.theResource->c_str());
theResource = new string(other.theResource->c_str());
}
++ResourceOwner(ResourceOwner&& other) {
++ printf(“move cons %s\n”, other.theResource->c_str());
++ theResource = other.theResource;
++ other.theResource = NULL;
++}
  ResourceOwner& operator=(const ResourceOwner& other) {
ResourceOwner tmp(other);
swap(theResource, tmp.theResource);
printf(“assign %s\n”, other.theResource->c_str());
}
++ResourceOwner& operator=(ResourceOwner&& other) {
++ printf(“move assign %s\n”, other.theResource->c_str());
++ theResource = other.theResource;
++ other.theResource = NULL;
++}
  ~ResourceOwner() {
if (theResource) {
printf(“destructor %s\n”, theResource->c_str());
delete theResource;
}
}
private:
string* theResource;
};
class ResourceHolder {
……
ResourceHolder& operator=(ResourceHolder&& other) {
printf(“move assign %s\n”, other.theResource->c_str());
resOwner = other.resOwner;
}
……
private:
ResourceOwner resOwner;
}

ResourceHolder移动 分配运算符中 ,我们想调用ResourceOwner移动 分配运算符,因为“ r值的无指针成员也应该是r值”。 但是,当我们简单地对resOwner = other.resOwner编码时,调用的实际上是ResourceOwner的常规赋值运算符 ,该运算符 resOwner = other.resOwner产生额外的副本。

这是再次重复问题陈述的好机会:

当1)我们知道变量实际上是r值时,而2)编译器则不是。 无法调用正确版本的重载函数。

作为解决方案,我们使用std::move将变量转换为r值,因此可以调用ResourceOwner赋值运算符的正确版本。

ResourceHolder& operator=(ResourceHolder&& other) {
printf(“move assign %s\n”, other.theResource->c_str());
resOwner = std::move(other.resOwner);
}

什么是std :: move准确?

我们知道类型转换不仅仅是告诉编译器“我知道自己在做什么”的编译器安慰剂。 它有效地生成的指令mov的值更大或更小的寄存器(例如, %eax - > %cl ),并进行“演员”。

那么std::move到底在幕后做了什么。 我在撰写本段时并不认识自己,所以让我们一起找出答案。

首先,我们对主体进行一些修改(我试图使样式保持一致)

例:

int main() {
ResourceOwner res(“res1”);
asm(“nop”); // remeber me
ResourceOwner && rvalue = std::move(res);
asm(“nop”); // remeber me
}

编译它,并使用分解obj

clang++ -g -c -std=c++11 -stdlib=libc++ -Weverything move.cc
gobjdump -d -D move.o

结果:

0000000000000000 <_main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 20 sub $0x20,%rsp
8: 48 8d 7d f0 lea -0x10(%rbp),%rdi
c: 48 8d 35 41 03 00 00 lea 0x341(%rip),%rsi # 354 <GCC_except_table5+0x18>
13: e8 00 00 00 00 callq 18 <_main+0x18>
18: 90 nop // remember me
19: 48 8d 75 f0 lea -0x10(%rbp),%rsi
1d: 48 89 75 f8 mov %rsi,-0x8(%rbp)
21: 48 8b 75 f8 mov -0x8(%rbp),%rsi
25: 48 89 75 e8 mov %rsi,-0x18(%rbp)
29: 90 nop // remember me
2a: 48 8d 7d f0 lea -0x10(%rbp),%rdi
2e: e8 00 00 00 00 callq 33 <_main+0x33>
33: 31 c0 xor %eax,%eax
35: 48 83 c4 20 add $0x20,%rsp
39: 5d pop %rbp
3a: c3 retq
3b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

我简单介绍一下两者之间会发生什么nop

  1. 将一个堆栈变量(大概是ResourceOwner res )的地址分配给%rsi
  2. %rsi的值分配给另一个堆栈变量(该变量是匿名的)
  3. 将匿名堆栈变量的值分配回%rsi (什么?)
  4. 将%rsi的值分配给另一个堆栈变量(大概是ResourceOwner && rvalue
  5. 因此,整个操作可以概括为“将ResourceOwner res的地址分配给ResourceOwner && rvalue ”,这与常规参考分配相同。

如果为编译器打开O (-O1) ,则所有这些伪指令都将消失。

clang++ -g -c -O1 -std=c++11 -stdlib=libc++ -Weverything move.cc
gobjdump -d -D move.o

此外,如果将临界线更改为常规参考分配:

ResourceOwner & rvalue = res;

正如上面第5点所假设的,除了变量偏移量有一些细微的差异外,生成的汇编代码基本相同。

测试表明, 移动语义是纯语法糖果,机器根本不在乎。

最后,

如果您喜欢阅读此书,请为它鼓掌或单击按钮关注我。 感谢您的光临,希望下次见到您。

这篇文章也存档在这里

From: https://hackernoon.com/one-shot-learning-of-c-r-value-and-move-27e5d6bcec3b

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值