当前位置: 首页 > news >正文

C++右值引用与移动语义

一、什么是左值和右值?

        左值和右值在字面上理解,是放在等号左边的值和放在等号右边的值,但是这种字面上的理解是不正确的,实际上,左值是指可以出现在赋值表达式左侧的表达式,通常表示一个具名的、有内存地址的对象,相反,右值是指只能出现在赋值表达式右侧的表达式,通常是临时的、没有持久内存地址的值。从定义中可以知道,左值和右值的最根本区别就在于是否有值持久的内存。

        例如:以下代码,a和b都是左值,虽然b不能放在等号左边被修改,但是也属于左值,因为其拥有持久的内存,但是getOne的返回值、2、1是右值,因为其没有持久的内存

        由于二者在内存上面的区别,就可以推导出二者另外一个区别:左值可以引用和取地址,右值不能引用和取地址(此处的引用其实是后面的左值引用)。所以,以下代码都是对的

        以上都是对左值进行取地址和引用,以下代码就是错的,因为以下代码在尝试对由右值进行取地址和引用,因为取地址和引用的原理都是需要获取两个对象的地址,右值本没有持久地址,获取右值地址就会报错。

 二、左值引用和右值引用

        在上面,提到过左值是可以引用的,但是右值不能引用,在C++11标准中引入了右值引用的概念,所谓右值引用,就是对右值的引用,使用&&标识,前文所提到的引用其实是特指左值引用。 例如,以下写法都是正确的,可以对右值引用。

        

        同时,通过左值引用和右值引用,可以重载不同的函数,例如下面的func函数,重载了左值引用和右值引用,在调用的时候,可以分别通过传递左值和右值区分二者,至于这样做的意义,文章后面会提到。

void func(int& a){
	cout << "调用左值引用函数" << endl;
}

void func(int&& a){
	cout << "调用右值引用函数" << endl;
}

int main(){
	int a = 5;
	func(a);
	func(5);
	return 0;
}

 三、移动语义

         假设现在有这样一个场景,有一个即将消亡的对象,需要复制给另外一个对象或者是用来创建一个新的对象,其一般的做法应该是利用该对象拷贝构造一个对象,其中拷贝的过程是按照将亡对象创建对象大小,再将值拷贝到新的对象中,完成拷贝构造,然后再调用将亡对象的析构函数,使其消亡,在这个过程中,新对象需要新的空间以及将亡对象里面的值,而将亡对象希望调用析构释放自身空间,那么可不可以在底层直接将将亡对象的空间给新对象呢?答案是可以的,这就是C++11的新特性——移动语义。将亡对象可能是右值也可能是左值,指的是马上结束生命周期的对象,例如即将出生命周期的左值,函数返回值的右值等。

        移动语义顾名思义,就是将一个将亡对象的资源给转移走,转移的资源可以用于创建新的对象,也可以用于向其他对象赋值。这样做的好处是,将亡值节省了回收资源的成本,创建对象和赋值减少了获取新的资源的成本。

        下面实现了一个很简单的代码,创建一个字符数组对象,分别创建buffer1,buffer2,buffer3,创建方式分别为一般构造,拷贝构造和移动语义构造(因为第三种直接用返回值构造会被编译器优化,所以手动将其再转为右值std::move())

class charBuffer{
public:
	charBuffer(int size = 10) :_size(size), _buffer(new char[size]){
		cout << "default constructor" << endl;
	}

	charBuffer(const charBuffer& other):_size(other._size),_buffer(new char[other._size]{
		memcpy(_buffer, other._buffer, other._size);
		cout << "copy constructor" << endl;
	}

	charBuffer(charBuffer&& other) :_size(other._size), _buffer(other._buffer){
		other._size = 0;
		other._buffer = nullptr;
		cout << "move constructor" << endl;
	}
	
	charBuffer& operator=(const charBuffer& other){
		//如果为自身,返回自身即可
		if (&other == this)
			return *this;
		//1.先尝试分配新的空间
		char* buffer = new char[other._size];

		//2.释放原来空间
		delete[] _buffer;
		_size = 0;

		//3.拷贝内容
		memcpy(buffer, other._buffer, other._size);
		_buffer = buffer;
		_size = other._size;

		cout << "copy assignment" << endl;

		return *this;
	}

	charBuffer& operator=(charBuffer&& other){
		//如果为自身,返回自身即可
		if (&other == this)
			return *this;

		//1.释放原来空间
		delete[] _buffer;
		_size = 0;

		//2.获取资源
		_buffer = other._buffer;
		_size = other._size;

		//3.将other置为空
		other._buffer = nullptr;
		other._size = 0;

		cout << "move assignment" << endl;

		return *this;
	}

	~charBuffer(){
		delete[]_buffer;
		_size = 0;
		cout << "destructor" << endl;
	}

private:
	int _size;
	char* _buffer;
};

charBuffer getBuffer(){
	charBuffer buffer(100);

	return buffer;
}

int main(){
	charBuffer buffer1(100);
	charBuffer buffer2(buffer1);
	charBuffer buffer3 = std::move(getBuffer());
	return 0;
}

         在除去编译器优化的内容,输出结果符合预期,buffer1使用一般构造,buffer2使用拷贝构造,在getBuffer中的对象使用一般构造,buffer3调用移动语义构造。其中buffer3中的_buffer就是函数中的buffer的_buffer。

         移动语义可以直接获取将亡值的资源,但是,其实并不是所有的类都需要实现移动语义,当一个类中只有基本类型时,其实移动语义的优势并没有那么明显,就不需要实现移动语义。

        而当一个类中维护着一段内存的时候,移动语义的优势就会变得很明显,因为其省去了拷贝内存内容的动作,使得资源可以再利用,例如在C++11的std库容器就广泛地使用了移动语义,因为其可以大大减小维护内存的成本。

四、std::move()

        通过查看move函数的源码,可以发现,其代码其实就只有一行,使用static_cast将一个_Ty类型的值转为_Ty&&,也就是将一个值转为右值引用的形式并返回。

        所以我们上面的代码,创建最后一个对象的代码可以改为以下,可以看到,运行的结果是相同的。

charBuffer buffer3 = static_cast<charBuffer&&>(getBuffer());

五、完美转发

         在上文中,讲到,函数的参数可以根据左值或者右值进行重载,从而执行不同的逻辑。

        那么以下面的代码为例,第一处调用传入左值,第二次调用传入右值,预期结果为打印先打印左值引用,再打印右值引用,但是结果是打印了两次左值引用,其原因是在调用g的时候,传入的是右值引用,但是在g函数中,创建了一个临时对象,就是这个临时对象,使得传入的buffer有了内存,此时的buffer是可以取地址的,所以再去调用f,传入的就是一个左值了。

void f(charBuffer& buffer){
	cout << "左值引用" << endl;
}

void f(charBuffer&& buffer){
	cout << "右值引用" << endl;
}

void g(charBuffer& buffer){
	f(buffer);
}

void g(charBuffer&& buffer){
	f(buffer);
}

         为了避免因为传参导致右值变为左值,就可以在g内部使用std::static_cast将其强转为右值引用的形式,但是这样做不太优雅,因为C++11提供了一个专门实现该方法的函数std::forward(),所以上面的代码可以改为以下代码,得到预期结果

void f(charBuffer& buffer){
	cout << "左值引用" << endl;
}

void f(charBuffer&& buffer){
	cout << "右值引用" << endl;
}

void g(charBuffer& buffer){
	f(std::forward<charBuffer&>(buffer));
}

void g(charBuffer&& buffer){
	f(std::forward<charBuffer&&>(buffer));
}

int main(){
	charBuffer buffer1(100);
	charBuffer buffer2(100);

	g(buffer1);
	g(std::move(buffer2));
	
	return 0;
}

        但是,以上写法会比较麻烦,当前只有一个参数需要区分左值右值,如果有多个参数需要区分左值右值,那么代码量会成指数倍增长,所以,可以使用函数模板的形式完成这一动作。

        观察到下面的代码中,g的参数是右值引用形式,该形式称为万能引用,即该形式既可以被推导为左值引用,也可以被推导为右值引用(注意:该写法只在定义时参数中起效,在其他地方还是右值引用的含义),这样就可以将参数推导为需要的类型。

void f(charBuffer& buffer){
	cout << "左值引用" << endl;
}

void f(charBuffer&& buffer){
	cout << "右值引用" << endl;
}

template<typename T>
void g(T&& buffer){
	f(std::forward<T>(buffer));
}

int main(){
	charBuffer buffer1(100);
	charBuffer buffer2(100);

	g(buffer1);
	g(std::move(buffer2));
	
	return 0;
}

六、引用折叠规则

        在上面的例子中,出现了万能引用的概念,其是一个右值引用的形式,但是传入的参数本身就是引用,再加上一个万能引用,变成了引用的引用,这在C++中是不允许的,所以C++11中存在一个规则称为引用折叠,两个引用叠加,会将其折叠为一个引用,其折叠规则如下,简单概括就是除了两个右值引用折叠外,其他折叠方式结果都为左值引用。注意:两个左值折叠得到的依然是左值引用,而不是右值引用,右值引用不能被拆为两个左值引用折叠。

         七、std::forward

        和move一样,forward的函数内容也十分简单,就是根据传入的参数,引用类型,对传入参数进行右值引用,再根据引用折叠规则,转为特定引用类型。

        所以上面代码也可以改为以下

template<typename T>
void g(T&& buffer){
	f(static_cast<T&&>(buffer));
}

笔者有话说

        到此处,本期内容就全部结束了,C++11引入左值引用和右值引用的概念使得C++这门语言变得更加复杂,让程序员的学习成本也变得更高,但是通过引入右值引用和移动语义,使得将亡资源可以被其他对象接手重复利用,提高了资源的利用效率,同时减小了资源请求与释放的开销。这是C++这门语言追求高效率的一种体现,但是这种对效率极致的追求反过来会使得C++的复杂度变得进一步提升。

相关文章:

  • PyTorch系列教程:使用预训练语言模型增强文本分类
  • 【QT】】qcustomplot的初步使用二
  • RedoLog
  • Java:读取中文,read方法
  • envoy 源码分析
  • python中序列操作和中高级用法
  • VSCode远程连接服务器 免密登录配置
  • AI小白的第七天:必要的数学知识(四)
  • PostgreSQL 14.17 安装 pgvector 扩展
  • 剑指Offer精选:Java与Spring高频面试题深度解析
  • Doris单价和集群的部署
  • 清晰易懂的 Swift 安装与配置教程
  • Spring Boot与Hazelcast整合教程
  • 4.1-4 SadTalker数字人 语音和嘴唇对应的方案
  • 深入理解【二分法】:从基础概念到实际应用
  • Android Listen AI 文字转语音-v2.0.1-开心版
  • 基于大模型的腮腺多形性腺瘤全周期诊疗方案研究报告
  • 网络安全应急入门到实战
  • 瑞萨RA系列使用JLink RTT Viewer输出调试信息
  • 【java面型对象进阶】------继承实例
  • 荣盛发展股东所持1.17亿股将被司法拍卖,起拍价约1.788亿元
  • 新片|《我仍在此》定档5月,《新·驯龙高手》同步北美上映
  • 李铁案二审驳回上诉,维持一审有期徒刑20年的判决
  • 北京银行一季度净赚超76亿降逾2%,不良贷款率微降
  • 69岁朱自强被查,曾任南京地铁总经理
  • 跟着京剧电影游运河,京杭大运河沿线六城举行京剧电影展映