云服务器

C++11 左值、右值与右值引用

2017-06-21 15:32:44 0

问题:

上图为bufferlist::read_fd的实现,此方法是从文件中读一定长度的内容,上图中的move是什么意思呢?

解释: 在C++11中,标准库在<utility>中提供了一个有用的函数std::move,这个函数的名字具有迷惑性,因为实际上std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。 简单来说就是减少不必要的拷贝,节省资源。

深入实践: 在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。其次,在C++11中右值又分为将亡值(xvalue,eXpiring Value)和纯右值(prvalue,Pure Rvalue)。其中纯右值的概念等同于我们在C98标准中所谓的右值概念,讲的是用于辨识临时变量和一些不跟对象关联的值。而将亡值是C++11中新提出的一个概念。将亡值概念的提出是为了解决什么问题?将亡值的定义到底是什么? 来看一段代码 #include <iostream> using namespace std; class Book { public: //构造函数(初始化p_int,并附上i的值) Book(int i) { p_int=new int(i); cout<<"call Book(int i)"<<endl; } //============================== //拷贝构造函数(对p_int实现了深拷贝) Book(const Book &a) { p_int=new int(*(a.p_int)); cout<<"call Book(const Book &a)"<<endl; } //============================== //析构函数(释放p_int) Book() { cout<<"call ~Book()"<<endl; delete p_int; } //============================== int *p_int; }; //传入一个Book对象,并直接返回该对象 Book make(Book a){return a;} int main() { Book test(10); make(test); return 1; } 运行结果 lkx@cloud001:$ g++ -g -std=c++11 -fno-elide-constructors -o books books.cpp lkx@cloud001:~$ ./books call Book(int i) call Book(const Book &a) call Book(const Book &a) call ~Book() call ~Book() call ~Book()

在这段代码中,我只显性的创建了一次Book对象,但是却产生了巨大的开销。为p_int开辟内存就达3次之多。我们仔细思考一下,两次拷贝构造函数的调用都是由于程序产生了一个副本导致的。而这些副本对象其实并没有任何实际意义,而且转瞬即逝,在完成了自己的传递任务后就即可销毁了。这些就是典型的将亡值! 如果已经可以明确,我的拷贝源是一个将亡值,那么我们其实并没有深拷贝的必要,而是只要将他的资源移位即用就可以。 所以C++11提供了一个所谓的移动构造函数,接受一个将亡值作为拷贝源。 来看一段代码 #include <iostream> using namespace std; class Book { public: //构造函数(初始化p_int,并附上i的值) Book(int i) { p_int=new int(i); cout<<"call Book(int i)"<<endl; } //============================== //拷贝构造函数(对p_int实现了深拷贝) Book(const Book &a) { p_int=new int(*(a.p_int)); cout<<"call Book(const Book &a)"<<endl; } //============================== //移动构造函数(把将亡值的指针据为己用) Book(Book &&a):p_int(a.p_int){ a.p_int=nullptr; cout<<"call Book(Book &&a)"<<endl; } //============================== //析构函数(释放p_int) ~Book() { cout<<"call ~Book()"<<endl; delete p_int; } //============================== int *p_int; }; //传入一个Book对象,并直接返回该对象 Book make(Book a){return a;}

int main() { Book test(10); make(test); return 0; } 运行结果 lkx@cloud001:$ g++ -g -std=c++11 -fno-elide-constructors -o books books.cpp lkx@cloud001:$ ./books call Book(int i) call Book(const Book &a) call Book(Book &&a) call ~Book() call ~Book() call ~Book()

和第一个例子的代码的区别是,编译器将make()中的return a;判定为将亡值,所以在拷贝副本调用了Book的移动构造函数,获得了屏幕输出call Book(Book &&a)。无疑这种判断是合理且符合我们预期的。节省了多余的内存拷贝开销。 那么一个值是否是将亡值,只能被动的依靠编译器裁决嘛?答案是否定的! 我们可以明确的声明一个将亡值(更加专业的叫法是右值引用),甚至能将一个明确知道不会再使用的左值变为右值引用拷贝源。

来看两段代码的比较。 代码1 #include <iostream> using namespace std; class Book { public: //构造函数(初始化p_int,并附上i的值) Book(int i) { p_int=new int(i); cout<<"call Book(int i)"<<endl; } //============================== //拷贝构造函数(对p_int实现了深拷贝) Book(const Book &a) { p_int=new int(*(a.p_int)); cout<<"call Book(const Book &a)"<<endl; } //============================== //移动构造函数(把将亡值的指针据为己用) Book(Book &&a):p_int(a.p_int) { a.p_int=nullptr; cout<<"call Book(Book &&a)"<<endl; } //============================== //析构函数(释放p_int) Book() { cout<<"call ~Book()"<<endl; delete p_int; } //============================== int *p_int; }; //传入一个Book对象,并直接返回该对象 Book make(Book a){return a;} int main() { //test是一个左值(一次普通构造) Book test(10); //test以纯右值副本1传入(一次拷贝构造),以右值引用副本2传出(一次移动构造),被左值a拷贝(一次移动构造) Book a=make(test); //到这里副本1和副本2都完成历史使命被析构了 cout<<"###############"<<endl; //a和test析构 return 1; } 代码1执行结果 lkx@cloud001:$ g++ -g -std=c++11 -fno-elide-constructors -o books books.cpp lkx@cloud001:~$ ./books call Book(int i) call Book(const Book &a) call Book(Book &&a) call Book(Book &&a) call ~Book() call ~Book() ############### call ~Book() call ~Book()

代码2 #include <iostream> using namespace std; class Book { public: //构造函数(初始化p_int,并附上i的值) Book(int i) { p_int=new int(i); cout<<"call Book(int i)"<<endl; } //============================== //拷贝构造函数(对p_int实现了深拷贝) Book(const Book &a) { p_int=new int(*(a.p_int)); cout<<"call Book(const Book &a)"<<endl; } //============================== //移动构造函数(把将亡值的指针据为己用) Book(Book &&a):p_int(a.p_int) { a.p_int=nullptr; cout<<"call Book(Book &&a)"<<endl; } //============================== //析构函数(释放p_int) ~Book() { cout<<"call ~Book()"<<endl; delete p_int; } //============================== int *p_int; }; //传入一个A对象,并直接返回该对象 Book make(Book a){return a;} int main() { //test是一个左值(一次普通构造) Book test(10); //test以纯右值副本1传入(一次拷贝构造),以右值引用副本2传出(一次移动构造),a成为副本2的别名 Book &&a = make(test); //到这里副本1完成了历史使命被析构了 //Book &&a = make(test);语句本质上延长了副本2的生命期,所以副本2没有析构 cout<<"###############"<<endl; //test和副本2析构 return 0; }

代码2执行结果 lkx@cloud001:$ g++ -g -std=c++11 -fno-elide-constructors -o books books.cpp lkx@cloud001:$ ./books call Book(int i) call Book(const Book &a) call Book(Book &&a) call ~Book() ############### call ~Book() call ~Book()

两段代码的屏幕输出的具体的原因已在代码注释中已经详细说明。在代码中我们明确的声明了右值引用类型变量a,并将它作为返回返回值副本的别名,延迟了副本的生命周期,减少了程序开销。

下面这段代码,展示当我们明确一个左值以后不会再被使用时,如何他它转换为右值引用拷贝源——利用std::move

#include <iostream> using namespace std; class Book { public: //构造函数(初始化p_int,并附上i的值) Book(int i) { p_int=new int(i); cout<<"call Book(int i)"<<endl; } //============================== //拷贝构造函数(对p_int实现了深拷贝) Book(const Book &a) { p_int=new int(*(a.p_int)); cout<<"call Book(const Book &a)"<<endl; } //============================== //移动构造函数(把将亡值的指针据为己用) Book(Book &&a):p_int(a.p_int) { a.p_int=nullptr; cout<<"call Book(Book &&a)"<<endl; } //============================== //析构函数(释放p_int) ~Book() { cout<<"call ~Book()"<<endl; delete p_int; } //============================== int *p_int; }; //传入一个Book对象,并直接返回该对象 Book make(Book a){return a;} int main() { //test是一个左值(一次普通构造) Book test1(10); //将test1作为右值引用拷贝源调用移动构造 Book test2(move(test1)); cout<<"##################"<<endl; }

执行结果 lkx@cloud001:$ g++ -g -std=c++11 -fno-elide-constructors -o books books.cpp lkx@cloud001:$ ./books call Book(int i) call Book(Book &&a) ################## call ~Book() call ~Book()

至此,把Book的拷贝次数降到最低!

 

上一篇: 无

微信关注

获取更多技术咨询