英文版发表在hackernoon并在个人博客存档。c++
本文是汉化重制版。markdown
首先,咱们先来看看啥是C++在性能和扩展性上越走越远,结果牺牲了易用性,一版比一版更不易学习。这篇文章主要讨论新版C++的几个相关的知识,右值,右值引用(&&),和move语义,但愿能帮助你一次搞定这几个难点。函数
简单点说,右值就是在等号右边的值。
oop
打上码:性能
int var; // too much JavaScript recently:)
var = 8; // OK! l-value (yes, there is a l-value) on the left
8 = var; // ERROR! r-value on the left
(var + 1) = 8; // ERROR! r-value on the left复制代码
够简单吧。咱们看一个更隐晦的状况,函数返回右值。
打上码:学习
#include <string> #include <stdio.h> int g_var = 8; int& returnALvalue() { return g_var; //here we return a left value } int returnARvalue() { return g_var; //here we return a r-value } int main() { printf("%d", returnALvalue()++); // g_var += 1; printf("%d", returnARvalue()); }复制代码
结果:spa
8
9复制代码
注意,我在例子里函数返回左值只是为了作演示,现实生活中请勿模仿。
指针
其实,在右值引用(&&)发明以前,右值就已经能够影响代码逻辑了。
好比这行代码:
c++11
const std::string& name = "rvalue";复制代码
没有问题,可是下面这行:code
std::string& name = "rvalue"; // use a left reference for a rvalue复制代码
是编译不过的:
error: non-const lvalue reference to type 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char> >') cannot bind to a value of unrelated type 'const char [7]'复制代码
说明编译器强制咱们用常量引用来指向右值。
再来个更有趣的🌰:
#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("rvalue"); // likewise for rvalue } 复制代码
运行结果:
lvalue detected:lvalue
rvalue detected:rvalue复制代码
说明这个差别足以让编译器决定重载函数。
不彻底是。这时就轮到&&
出场了。
打上码:
#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()); } int main() { std::string name = “lvalue”; const std::string cname = “cvalue”; print(name); print(cname); print(“rvalue”); }复制代码
运行结果:
lvalue detected:lvalue
const value detected:cvalue
rvalue detected:rvalue复制代码
说明若是有专门为右值重载的函数的时候,右值的传参会去选择专有函数(接受&&
参数的那个),而不去选更通用的接受常量引用做为参数的函数。因此,&&
能够更加细化右值和常量引用。
我总结了函数实参(实际传的那个变量)和形参(括号里声明的那个变量)的适配性,有兴趣的话你也能够经过改上面的🌰验证下:
把常量引用细分红常量引用和右值是很好,可是仍是没回答具体有啥用。
问题是当参数为右值时,没必要要的深拷贝。
讲具体点,&&
用来区分右值,这样在这个右值 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 res1destructor res1=====start testAssign()=====copy res1assign res1destructor res2=====destructors for stack vars, ignore=====destructor res1destructor res1=====start testRValue()=====copy res1assign res1destructor res2destructor res1=====destructors for stack vars, ignore=====destructor res1复制代码
前两个例子testCopy()
和testAssign()
里面的结果没问题。这里将res1
里面的的资源拷贝到res2
里是合理的,由于这两个独立的个体都须要有各自的独享资源(string)。
可是在第三个例子就不对了。此次深拷贝的对象res1是个右值(ResourceOwner(“res1”)
的返回值),其实它立刻就要被回收了。因此自己是不须要独享资源的。
我把问题描述再重复一次,此次应该就好理解了:
&&
用来区分右值,这样在这个右值 1)是一个构造函数或赋值函数的参数,和2)对应的类包含指针,并指向一个动态分配的资源(内存)时,就能够在函数内避免深拷贝。
若是深拷贝右值的资源不合理,那什么操做是合理的呢?答案是
继续讨论move语义。解决方法很简单,若是参数是右值,就不拷贝,而是直接“搬”资源。咱们先把赋值函数用右值引用重载下:
ResourceOwner& operator=(ResourceOwner&& other) {
theResource = other.theResource;
other.theResource = NULL;
}复制代码
这个新的赋值函数就叫作move赋值函数。move构造函数也能够用差很少的办法实现,这里先不赘述了。
若是不太好理解的话,能够这么来:好比你卖了个旧房子搬新家,搬家的时候不必定要把家具都丢掉再买新的对伐(咱们在🌰3里面就丢掉了)。你也能够把家具“搬”到新家去。
完美。
最后咱们来解决这个std::move。
咱们仍是先看看问题:
当1)咱们知道一个参数是右值,可是2)编译器不知道的时候,这个参数是调不到move重载函数的。
一个常见的状况是在resource owner上面再加一层类ResourceHolder
holder
|
|----->owner
|
|----->resource复制代码
注意,在下面的代码里,我把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
的move赋值函数中,其实咱们想调用的是的move赋值函数,由于右值的成员也是右值。可是
resOwner = other.resOwner
实际上是调用了普通赋值函数,仍是作了深拷贝。
那再重复一次问题,看看是否是好理解了:
当1)咱们知道一个参数是右值,可是2)编译器不知道的时候,这个参数是调不到move重载函数的。
解决方法是,咱们能够用std::move
把这个变量强制转化成右值,就能调用到正确的重载函数了。
ResourceHolder& operator=(ResourceHolder&& other) { printf(“move assign %s\n”, other.theResource->c_str()); resOwner = std::move(other.resOwner); }复制代码
彻底能够!
咱们都知道强转除了让编译器闭嘴,实际上是会生成对应的机器码的。(在不开O的状况下比较容易观察到)这些机器码会把变量在不一样大小的寄存器里面移来移去来真正完成强转操做。
因此std::move
也和强转作了相似的操做吗?我也不知道,一块儿来试试看。
首先,咱们把main函数改一改,(我尽可能保持逻辑一致)
打上码:
int main() { ResourceOwner res(“res1”); asm(“nop”); // remeber me ResourceOwner && rvalue = std::move(res); asm(“nop”); // remeber me }复制代码
编译它,而后用下面的命令把汇编语言打出来
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
作了染色。看两个nop
中间那段确实生成了一些机器码,可是这些机器码貌似啥都没作,只是简单的把一个变量的地址赋值给另外一个而已。而且,若是咱们把O(-O1就够了)打开,全部的nop中间的机器码就都被干掉了。
clang++ -g -c -O1 -std=c++11 -stdlib=libc++ -Weverything move.cc gobjdump -d -D move.o复制代码
再来,若是把关键行改为
ResourceOwner & rvalue = res;复制代码
除了变量的相对偏移有变化,其实生成的机器码是同样同样的。
说明了在这里std::move
实际上是个纯语法糖,而并无啥实际的操做。
好了,今天先写到这里。若是你喜欢本篇,欢迎点赞和关注。有兴趣也能够去Medium上随意啪啪啪个人其余文章。感谢阅读。