云服务器

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

2017-12-25 10:38:25 0

回顾:

上一篇讲到ceph使用c++11标准,为深入了解ceph的工作流程打下基础,先要掌握一些必要的c++11新特性,上篇说到了using的新功能(类似typedef的功能)。接下来学习一下移到构造相关的知识。

 

问题:

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

 

下期预告:

C++11 Lambda函数与表示式

上一篇: 无

微信关注

获取更多技术咨询