C++11 左值、右值与右值引用
问题:

上图为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的拷贝次数降到最低!