C++ 11 右值引用和移动语义的实现

什么是左值,什么是右值?

左值就是程序能获得其地址的表示数据的表达式,包括变量,const常量,解除引用的指针。

相反,右值就是不能应用地址运算符&的表示数据的表达式,包括字面常量,x+y,非引用的返回值。

 

什么是左值引用,什么是右值引用?

我们常说的C++的引用,大部分时候指的就是左值引用,符号是&,

比如 int a=10;int &b=a; 其中,b就是a的引用,可以理解为别名。

右值引用符号是&&,

比如 int &&a = 10; 其中,a就是10的右值引用。&10是非法的,但是&a却是合法的。

 

移动语义和右值引用的关系

移动语义对降低C++构造和析构的开销有重要的意义,减少了传值、返回值过程中的资源拷贝。

C++移动语义的实现,正是基于右值引用。

 

传值和返回值的代码开销在哪里?

请看下面这段代码;

#include <iostream>
using std::cin;
using std::cout;
using std::endl;

struct Person {
	Person(const char* p) {
		cout << "constructor" << endl;
	}
	Person(const Person& p) {
		cout << "copy constructor" << endl;
	}
	const Person& operator=(const Person& p) {
		cout << "operator=" << endl;
		return *this;
	}
	~Person() {
		cout << "destructor" << endl;
	}
};

Person getAlice() {
    Person p("alice");     // 对象创建。调用构造函数,一次 new 操作
    return p;              // 返回值创建。调用拷贝构造函数,一次 new 操作
                           // p 析构。一次 delete 操作
}

int main() {
	cout << "______________________" << endl;
	Person a = getAlice(); // 对象创建。调用拷贝构造函数,一次 new 操作
                           // 返回值析构,一次 delete 操作
                           // 当前步骤合共 3次构造,2次析构
	cout << "______________________" << endl;
	a = getAlice();        // 对象创建。调用拷贝构造函数,一次 new 操作
                           // 返回值析构,一次 delete 操作
                           // 当前步骤合共 3次构造,2次析构
	cout << "______________________" << endl;
    return 0;
                           // a 析构。一次 delete 操作
}

在不考虑NVRO(返回值优化)的情况下,上面这段代码的预期过程如注释,总共6次构造,5次析构。

当然了,编译器会进行NVRO(返回值优化),减少构造和析构次数。

不同编译器的NVRO结果是不一样的:

在Visual Studio 2015上面编译运行结果是:

Person a = getAlice(),这一步,getAlice里面p的析构和返回值的构造被优化掉了,相当于a直接用了getAlice()的对象;

a=getAlice(),这一步,没有NVRO优化。

 

g++(8.2.0)优化程度比VS高。

Person a = getAlice(),这一步,getAlice() 里面p的析构,返回值的构造和析构,a的拷贝构造都被优化掉了;

a=getAlice(),这一步,NVRO优化程度比赋初值操作的低,

getAlice() 里面p的析构和返回值的构造被优化掉了,相当于a直接用了getAlice里面的对象;

 

上面的代码还能优化吗?

可以。

通过移动语义,可以把拷贝构造函数改写成移动构造函数;或者就是另外写一个移动构造函数,实现重载。参考[4]

使用std::move相当于显式使用移动语义。std::move()实际上是static_cast<T&&>()的简单封装。

 

用右值引用实现移动语义,从而优化拷贝构造函数

参见以下代码和注释

	// 基于左值引用的拷贝构造函数
	//(参数p设置const属性,不允许直接取用参数p的指针成员,这是为了拷贝构造函数既能接受左值参数,也能接受右值参数)
	//(不设置const属性也行,但是就不能用右值(getAlice的返回值)进行拷贝构造得到新的对象了。)
	const Person& operator=(const Person& p) {
		cout << "operator=" << endl;
		delete[] name;

		int len = strlen(p.name) + 1;
		name = new char[len];
		memcpy(name, p.name, len); //左值引用的拷贝构造,会有一次申请内存和数据拷贝
		return *this;
	}
	// 基于右值引用的拷贝构造函数
	//(不需要const了,那么就可以直接取用参数p的指针成员,且可以在取用后将p的指针成员置为nullptr,这样该块内存就不会被析构了)
	const Person& operator=(Person&& p) {
		cout << "operator=" << endl;
		delete[] name;

		name = p.name; //直接取用
		p.name = nullptr; //置空使得系统无法将该块内存析构掉
							//相对比左值引用,右值引用的拷贝构造可以实现更加高效:少了一次内存申请和拷贝。
		return *this;
	}

 

【参考】

[1]《C++ Primer Plus》,18.1.9,右值引用一节

[2] https://harttle.land/2015/10/11/cpp11-rvalue.html 这篇文章讲的比较易于理解

[3] 如何评价 C++11 的右值引用(Rvalue reference)特性? - Tinro的回答 - 知乎 https://www.zhihu.com/question/22111546/answer/30801982

[4] https://www.cnblogs.com/dongdongweiwu/p/4743661.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章