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

【C++】右值引用与完美转发

目录

一、右值引用:

1、左值与右值:

2、左值引用和右值引用:

二、右值引用的使用场景:

1、左值引用的使用场景:

2、右值引用的使用场景:

移动构造

移动赋值

三、完美转发:

1、万能引用:

2、实际使用:


一、右值引用:

1、左值与右值:

在了解右值引用前,要先了解什么是左值引用(其实在之前已经使用过很多回了),那么要了解什么是左值引用,就先要了解什么是左值什么是右值

在等号左边的值叫左值吗?在等号右边的值叫右值吗? 

显然定义不会这么简单的,但是左值可以出现赋值符号的左边,右值不能出现在赋值符号左边,也就是说在等号左边的值一定不是右值,在等号右边的可以是左值或右值

左值:

能够进行取地址的操作,也可以被修改

如下的a,b,c都是左值

int main()
{
	int a = 10;
	const int b = 10;
	int* c = new int(0);
    return 0;
}

右值:

不能够进行取地址的操作,一般不能够被修改

如下,10,x,fmin(x,y)的返回值就是右值

int main()
{
    double x = 1.1, y = 2.2;
	//如下就是右值
	10;
	x + y;
	fmin(x, y);
    return 0;
}

理解:

1、右值的本质是一个临时变量或者常量值
2、这些临时变量是没有被实际存储起来的,所以无法对右值取地址        
3、像上述的fmin的返回值,其实际上就是一份临时拷贝,所以算作右值

2、左值引用和右值引用:

什么是左值引用:

左值引用就是给左值取别名

int main()
{
	int a = 10;
	const int b = 10;
	int* c = new int(0);

	//这就是左值引用
	int& pa = a;
	const int& pb = b;
	int*& pc = c;

    return 0;
}

什么是右值引用:

右值引用就是给右值取别名

int main()
{
	double x = 1.1, y = 2.2;
	//如下就是右值
	10;
	x + y;
	fmin(x, y);

	//这个就是右值引用
	int&& p1 = 10;
	int&& p2 = x + y;
	int&& p3 = fmin(x, y);
    return 0;
}

这里在给右值取别名后,右值会被存储到特定的位置,此时就能够取到该位置的地址了

左值引用可以引用右值吗 ----- 可以

但是,左值引用不能直接引用右值,因为右值不能够被修改,左值可以修改,如果直接引用的话权限会存在放大问题,所以如果想要左值引用右值就需要加上const修饰

像我们之前在函数参数传参的时候经常写const T& x,这就是保证既能够传左值,又能够传右值

template<class T>
void func(const T& val)
{
	cout << val << endl;
}
int main()
{
	string s("111");
	func(s);       //s为左值

	func("222"); //"222"为右值
	return 0;
}

右值引用可以引用左值吗 ----- 可以

但是,右值引用也不能直接引用左值,如果想要引用左值,就需要加上move后的左值

int main()
{
	int a = 10;
	//右值引用给左值取别名
	int&& pa = move(a);
    return 0;
}

为什么加上move后才能让右值引用来引用左值呢?

我们首先要知道,左值引用或者右引用都是在给资源取别名,对于左值引用,就是直接指向原本的数据,对于右值引用,我们知道原本是没有空间资源的,那么右值引用引用右值就是首先开辟一块空间,然后将常量或者临时变量转移到开辟好的地方,然后在指向该地方

所以右值引用的本质是对右值进行资源的转移

此时就有空间资源了,此时就能够取地址了,并且能够对其进行修改了

对于常量,临时变量,表达式的结果这些右值,编译器在右值引用的时候会直接将这些右值进行转移资源,但是对于左值,编译器不敢直接转移,这个时候编译器就为用户提供了一个函数move,当进行move左值的时候,就能够让右值引用 引用左值了

二、右值引用的使用场景:

1、左值引用的使用场景:

左值引用既能够引用左值,又能够引用右值,但是还是存在短板,所以在C++11里面,引入了右值引用来弥补左值引用的短板

在左值引用中:

1、左值引用做参数,防止传参是的拷贝
2、左值引用做返回值,防止返回时对返回对象进行拷贝

首先,写一个自己的string类,在里面写上部分cout来方便打印观察

namespace ppr
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;//返回字符串中第一个字符的地址
		}
		iterator end()
		{
			return _str + _size;//返回字符串中最后一个字符的后一个字符的地址
		}
		//构造函数
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		//交换两个对象的数据
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		//拷贝构造函数(现代写法)
		string(const string& s)
			:_str(nullptr),_size(0),_capacity(0)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp;
			swap(tmp);
		}

		//移动构造函数(现代写法)
		string(string&& s)
			:_str(nullptr), _size(0), _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

		//赋值运算符重载(现代写法)
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;
			string tmp;
			swap(tmp);
			return *this;
		}
		//析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}
		//[]运算符重载
		char& operator[](size_t i)
		{
			assert(i < _size);
			return _str[i];
		}
		//改变容量,大小不变
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strncpy(tmp, _str, _size + 1);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		//尾插字符
		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			_str[_size] = ch;
			_str[_size + 1] = '\0';
			_size++;
		}
		//+=运算符重载
		string operator+=(char ch)
		{
			push_back(ch);
			string tmp(*this);
			return tmp;
		}
		//返回C类型的字符串
		const char* c_str()const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

接着看看左值引用的使用场景

//首先,看看左值引用的使用场景
//值传参
void func1(ppr::string s)
{
	cout << "void func1(ppr::string s)" << endl;
}
//左值引用传参
void func2(const ppr::string& s)
{
	cout << "void func2(const ppr::string& s)" << endl;
}

int main()
{
	ppr::string ret1("1111111111111");
	func1(ret1);//这里采用深拷贝
	func2(ret1);
	ret1 += '0';//里面return *this 的时候,也会进行拷贝构造

	return 0;
}

其中,在func1的时候,传值传参会进行一次拷贝构造,在+=那里,返回* this的时候,也会进行一次拷贝构造这样的话会看到两次深拷贝

左值引用短板:

左值引用能够避免不必要的拷贝构造,但是并不能完全避免

左值引用做参数的时候,能够完全避免传参时的拷贝
左值引用做返回值的时候,不能完全避免拷贝

比如如果返回的是一个局部变量,在返回的时候局部变量被销毁了,此时如果使用左值引用进行返回,就会返回的野指针,此时就不能够用左值引用返回,需要老老实实地值拷贝

如下会进行两次拷贝操作,然后将ret返回

如果在新一点的编译器会进行优化,只需进行一次拷贝操作

如果是引用传参,此时局部变量的局部空间,在出了函数作用域之后就会被释放,此时就会出问题

所以,C++11为了解决这类问题,提出了右值引用来解决这种场景

2、右值引用的使用场景:

右值分为 纯右值将亡值

纯右值:内置类型的右值
将亡值:自定义类型的右值

右值引用和移动语句解决上述问题的方式就是,给当前模拟实现的string类增加移动构造方法

移动构造

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

如果没加上述的移动拷贝,就会出现深拷贝

如果加上移动构造,那么就会走移动构造函数,这样就能更加减少拷贝

移动构造的本质就是将参数的右值窃取过来,占为己有,这样它就不用再深度拷贝了,所以叫做移动构造

移动构造和拷贝构造的区别:

1、在没有增加移动构造之前,由于拷贝构造采用的是const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数
2、增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数
3、string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小

左值引用:直接引用对象以减少拷贝

右值引用:间接减少拷贝,将临时资源等将亡值的资源通过 移动构造 进行转移,减少拷贝

移动赋值

移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,之所以它叫移动赋值,就是窃取别人的资源来赋值给自己的意思

// 赋值重载
string& operator=(const string& s) 
{
	cout << "string& operator=(string s) -- 深拷贝" << endl; 
	string tmp(s); 
	swap(tmp); 

	return *this;
}
//移动赋值
string& operator=(string&& s) 
{
	cout << "string& operator=(string && s) -- 移动拷贝" << endl; 
	swap(s); 

	return *this;
}

string& operator=(const string& s) 和string& operator=(string&& s) 的区别:

1、在没有string& operator=(string&& s) 的时候,如果进行=操作,那么无论是左值还是右值传参都会调用string& operator=(const string& s) 这个函数

2、在增加移动赋值后,如果是左值就调用原来的函数,如果是右值就调用新加的移动赋值函数

3、移动赋值函数是通过swap函数进行资源的交换,而原来的operator=是通过深拷贝进行,因此,移动赋值的代价比原来的要小

三、完美转发:

1、万能引用:

template<class T>
void PerfectForward(T&& t)
{
	//...
}

这里函数中的参数并不是右值引用,如果传的模板是左值,这里的参数就是左值引用,相反如果传的模板是右值,那么这里的参数就是右值引用

void func(int& a)
{
	cout << "左值引用" << endl;
}
void func(const int& a)
{
	cout << "const 左值引用" << endl;
}
void func(int&& a)
{
	cout << "右值引用" << endl;
}
void func(const int&& a)
{
	cout << "const 右值引用" << endl;
}
template<class T>
void perfectForward(T&& val)
{
	func(val);
}

int main()
{
	int a = 10;
	perfectForward(a); //左值
	const int b = 10;  //const 左值
	perfectForward(b);

	perfectForward(move(a)); // 右值
	perfectForward(move(b)); //const 右值
	return 0;
}

如上,这就是通过func函数重载,来观察编译器会怎样进行函数调用

如上,这是运行结果,为什么会这样呢?难道是编译器做的不对吗,在实际调用中,4个函数没有一个是进入了右值引用,均匹配的是左值引用版本,这是为什么呢?

当对右值进行引用后,会导致右值被存储到特定的位置,此时就能够对这个引用后的右值进行取地址了,这样的话,这个右值就模版被识别成左值了

也就是说,在右值引用过一次后,会导致右值变成左值,但是如果想要继续保证其右值的属性,此时就需要用到完美转发

如上,在对右值引用进行传参的时候,在前面加上forward<T>,这样经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数

forward是一个模板函数,需要指定模板参数类型T,确保能正确推导并传递

2、实际使用:

首先实现一个建议的list,在其中实现左值引用的push_back和insert函数

namespace ppr
{
	template<class T>
	struct ListNode
	{
		T _data;
		ListNode* _next = nullptr;
		ListNode* _prev = nullptr;
	};
	template<class T>
	class list
	{
		typedef ListNode<T> node;
	public:
		//构造函数
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		//左值引用版本的push_back
		void push_back(const T& x)
		{
			insert(_head, x);
		}
		//右值引用版本的push_back
		void push_back(T&& x)
		{
			insert(_head, x);
		}
		//左值引用版本的insert
		void insert(node* pos, const T& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
		//右值引用版本的insert
		void insert(node* pos, T&& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
	private:
		node* _head; //指向链表头结点的指针
	};
}

接着进行左值和右值的push_back版本的调用

int main()
{
	ppr::list<ppr::string> lt;
	ppr::string s1("111111111111111");//左值的push_back
	lt.push_back(s1);

	cout << endl << endl;
	ppr::string s2("111111111111111");//右值的push_back
	lt.push_back(move(s2));

	cout << endl << endl;
	lt.push_back("22222222222222222");//右值的push_back

	return 0;
}

但是会发现全部都是深拷贝,这和上述右值被引用后,就可以取地址了,就变成左值了,所以为了避免这种情况,就需要在右值版本的push_back和insert加上完美转发,让右值能够保存右值属性

相关文章:

  • 软件工程面试题(十)
  • 妙用《甄嬛传》中的选妃来记忆概率论中的乘法公式
  • 交换技术综合实验
  • 第四章.4.3.1ESP32传感器数据采集与滤波处理实战教程
  • 从0开始——在PlatformIO下开展STM32单片机的HAL库函数编程指南
  • ​​​​​​​​​​​​​​Spring Boot数据库连接池
  • Vue学习笔记集--computed
  • 蓝桥杯-特殊的多边形(dfs/前缀和)
  • 指针和引用
  • 业务流程先导及流程图回顾
  • YOLO基础知识
  • 【C语言文件精选题】
  • 《网络管理》实践环节01:OpenEuler22.03sp4安装zabbix6.2
  • 验证Linux多进程时间片切换的程序
  • PyTorch 张量的new_tensor方法介绍
  • 算法基础——树
  • RAG基建之PDF解析的“流水线”魔法之旅
  • 网络安全-网络安全基础
  • freecad gear模块 生成齿轮导出fcstd step
  • 20组电影美学RED摄像摄影机视频胶片模拟色彩分级调色LUT预设包 Pixflow – CL – RED Camera LUTs
  • 【社论】法治是对民营经济最好的促进
  • 五一去哪儿|外国朋友来中国,“买买买”成为跨境旅游新趋势
  • 城市更新·简报│中央财政支持城市更新,倾斜超大特大城市
  • 国台办:相关优化离境退税政策适用于来大陆的台湾同胞
  • 中老铁路跨境国际旅客突破50万人次
  • 2025年“投资新余•上海行”钢铁产业“双招双引”推介会成功举行