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() {
testRValue();
testCopy();
testAssign();
}
结果:
=====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,在下面的示例中,我也完成了ResourceOwner
的move 构造函数的实现)
例:
#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
。
- 将一个堆栈变量(大概是
ResourceOwner res
)的地址分配给%rsi
- 将
%rsi
的值分配给另一个堆栈变量(该变量是匿名的) - 将匿名堆栈变量的值分配回
%rsi
(什么?) - 将%rsi的值分配给另一个堆栈变量(大概是
ResourceOwner && rvalue
) - 因此,整个操作可以概括为“将
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