前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++ 左值和右值

C++ 左值和右值

原创
作者头像
永远的Alan
修改2023-04-18 01:04:55
1.1K0
修改2023-04-18 01:04:55
举报
文章被收录于专栏:姓程名序员

左值和右值

在C++11之前,一个变量分为左值右值左值是可以放在=运算符左边的值,有名字,可以用&运算符取地址(如 int n = 10;n即为左值);右值则是只能放在=运算符右边,没有名字,不能用&运算符取地址的值,一般是临时变量(非引用返回的函数返回值、表达式等,例如函数int func()的返回值,表达式a+b的返回值)、lambda表达式、不跟对象关联的字面量值,例如true,100等。

C++11以后对C++98中的右值进行了扩充,在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于C++98标准中右值的概念;将亡值则是C++11新增的跟右值引用相关的表达式,通常是将要被移动的对象,比如返回右值引用T&&的函数返回值、std::move()的返回值,或者转换为T&&的类型转换函数的返回值(如static_cast<int&&>(10))。

代码语言:javascript
复制
//左值
int a = 10;
int b = a;
++a;        //前置自增/自减为左值
--a;
"abcdefg";  //注意常量字符串为左值,可以对其取地址

//右值
a++;        //后置自增/自减为右值
a--;
a + b;
100;        //其他的常量类型为右值
5.0;

左值引用和右值引用

右值引用是c++11中新加入的类型,主要作用是减少对象复制时不必要的内存拷贝,以实现对象之间的快速复制:对于函数按照值返回的形式,必须创建一个临时对象,临时对象在创建好之后,则进行了一次拷贝构造,再将该临时对象拷贝构造给接收的对象,则又进行了一次拷贝构造,此时临时对象被销毁。就相当于返回一个临时对象时,会调用两次拷贝构造,对空间而言是一种浪费,程序的效率也会降低,并且临时对象的作用不是很大。

左值引用右值引用都属于引用类型,都必须在声明时进行初始化,而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。

一般情况下,左值引用只能接受左值对其进行初始化,右值引用只能接受右值对其进行初始化;但常左值引用是个例外,它是“万能”的引用类型:它可以接受非常量左值、常量左值、右值对其进行初始化,不过只能通过引用来读取数据,无法去修改数据,因为其被const修饰。在c++11以后,右值在函数参数匹配时会优先与右值引用绑定,而不是const左值引用。

注意:

  • 左值引用右值引用本身都为左值,都可以取地址。只是左值引用绑定的对象一般为左值(常左值引用可以绑定到右值对象),而右值引用绑定的对象为右值。即引用类型对象本身的左右值属性与其绑定的对象的左右值属性无关。
  • 右值引用(或const左值引用,只读)绑定到一个临时变量时,本来会被销毁的临时变量的生存期会延长至这个引用的生存期。
  • 对于自定义类型T的对象t:如果t为左值,那么t的非静态成员也为左值,如果t为右值,那么t的非静态成员也为右值。

代码语言:javascript
复制
int a = 10, b = 30;
int& n = a;         //左值引用
int& n = 10;        //错误,编译不过

int&& m = 100;      //右值引用
m = m + 50;         //m = 150
const int& k = 100; //这样也行,但是k只读,不能修改
int&& m = a + b;    //m为右值引用

//以下函数返回一个将亡值(右值)
constexpr int&& func(int& _Arg)
{
    return (static_cast<int&&>(_Arg));
}
代码语言:javascript
复制
std::string func()
{
    return std::string("XXXX");
}

//func()返回的临时变量的生命周期会延长至pStr的生命周期
std::string&& pStr = func();
std::cout << "pStr : " << pStr << std::endl;
pStr = "OOOO";
std::cout << "pStr : " << pStr << std::endl;

输出结果为
pStr : XXXX
pStr : OOOO

不要返回局部变量的引用

代码语言:javascript
复制
const std::string& func1()
{
	return static_cast<std::string&>(std::string("XXXX"));
}

std::string&& func2()
{
	return static_cast<std::string&&>(std::string("XXXX"));
}

int main()
{
	const std::string& aa = func1();
	std::string&& aa2 = func2();
	
	//std::cout << "aa: " << aa << std::endl;	会报错,因为aa引用的对象已销毁 
	//std::cout << "aa2: " << aa2 << std::endl;	会报错,因为aa2引用的对象已销毁
	
	return 0;
}

万能引用

对于一个实际的类型 T,它的左值引用是 T&,右值引用是 T&&

那么:是不是看到 T&就一定是个左值引用?是不是看到 T&&就一定是个右值引用?

对于前者的回答是 “是”,对于后者的回答为 “否”:

  • 如果一个变量或者参数被声明为T&&,其中T是需要被推导的类型,那这个变量或者参数就不一定是右值引用,而是一个万能引用(universal reference),如template<typename T> void foo(T&& param);auto&& var2 = var1;。万能引用不是一种引用类型,而是会根据T的推导结果,决定其究竟是一个左值引用还是右值引用。
  • 万能引用虽然跟右值引用的形式一样,但右值引用需要是确定的类型,如: int&& ref = x;(int为确定类型,不需要推导)。
  • 万能引用只有一种形式:T&&,不能携带任何的修饰符: const T&&std::vector<T>&&等为右值引用

代码语言:javascript
复制
#include <string>
#include <iostream>
#include <type_traits>

template <typename T>
void mytest(T&& t)
{
	//判断T是否为引用类型
	std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;

	std::cout << "t: " << t << std::endl;
}

int main()
{
	int a = 11;

	mytest(a);//此时T&& t接收到的实参为左值,T被推导为int&,t的类型为int& &&,引用折叠为int&
	mytest(22);//此时T&& t接收到的实参为右值,T被推导为int(并非int&&),t的类型为int&&
	std::cout << std::endl;

	int& m = a;
	int&& n = 22;
	n = n + 10;//右值引用可以修改绑定对象的值

	mytest(m);//此时T&& t接收到的实参为左值(m本身为左值),T被推导为int&,t的类型为int& &&,引用折叠为int&
	mytest(n);//此时T&& t接收到的实参为左值(n本身为左值),T被推导为int&,t的类型为int& &&,引用折叠为int&
	std::cout << std::endl;

	mytest(static_cast<int&>(m));//此时T&& t接收到的实参为左值,T被推导为int&,t的类型为int& &&,引用折叠为int&
	mytest(static_cast<int&&>(n));//此时T&& t接收到的实参为右值,T被推导为int,t的类型为int&&
	std::cout << std::endl;

	return 0;
}

运行结果如图:

代码语言:javascript
复制
#include <string>
#include <iostream>
#include <type_traits>

template <typename T>
void mytest2(T&& t)
{
	std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
	std::cout << "is_const: " << std::is_const<T>::value << std::endl;
	std::cout << "t: " << t << std::endl;
}

int main()
{	
	const int a = 11;
	mytest2(a);
	mytest2(10);
	return 0;
}

运行结果如图:

我们发现我们传进去的const的修饰符也消失了,但实际上并不是如此,这里就要有顶层const和底层const的区别了,引用是只有底层const的,不存在顶层const,而is_const这个方法它只能检测到顶层const,所以输出了false;这里的T应该为const int&

代码语言:javascript
复制
//这里T&&并不是万能引用,因为T的类型在类模板实例化时已经确定,调用函数void fun(T&& t)时,T已为确定类型,不用再推导   
typemplate <typename T>
class A
{
    void func(T&& t);
}

//但如下是万能引用
typemplate <typename T>
class A
{
    typemplate <typename U>
    void func(U&& u);
}

引用折叠

引用折叠只能应用于推导的语境下(如:模板实例化,autodecltype等),其他情况非法(如 int& & n = a;)。

右值引用加上右值引用等于右值引用,除了这个外其他的所有形式都为左值引用:

T& & ---> T&

T& && ---> T&

T&& & ---> T&

T&& &&---> T&&

对于template <typename T> void func (T& t)形式 (即T&),不管T是什么类型,T&都会变成T&

int& & ---> int&

int&& & ---> int&

int & ---> int&

因此这个函数模板只能接收左值实参。

对于template <typename T> void func (T&& t)形式 (即T&&),如果 T 被显示指定为int&& ,那 T&& 的结果仍然是右值引用,int&& && 折叠成了 int&&

如果T是根据实参推导时:

若实参为左值, T 推导出是左值引用(如int&),T&& 的结果仍然是左值引用,int& && 折叠成了 int&

若实参为右值, T 推导出是实际类型(如int), T&& 的结果仍然是右值引用int&&

代码语言:javascript
复制
template <typename T>
void mytest2(T&& t)
{
	std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
	std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
	std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;
	std::cout << "t: " << t << std::endl;
}

int main()
{	
	int a = 11;
	mytest2(a);//此时T&& t接收到的实参为左值,T被推导为int&,t的类型为int& &&,引用折叠为int&
	mytest2(10);//此时T&& t接收到的实参为右值,T被推导为int(并非int&&),t的类型为int&&
	mytest2<int&&>(static_cast<int&&>(30));//此时T&& t接收到的实参为右值,T被显示指定为int&&,t的类型为int&& &&,引用折叠为int&&   
	
	return 0;
}

运行结果如图:

完美转发

首先解释一下什么是完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

代码语言:javascript
复制
#include <string>
#include <iostream>
#include <type_traits>

void func(int& a)
{
	std::cout << "lvalue = " << a << std::endl;
}

void func(int&& a)
{
	std::cout << "rvalue = " << a << std::endl;
}

template <typename T>
void mytest2(T&& t)
{
	std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
	std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
	std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;

	func(std::forward<T>(t));
	//func(t);
}

int main()
{	
	int a = 11;
	mytest2(a);//此时T&& t接收到的实参为左值,T被推导为int&,t的类型为int& &&,引用折叠为int&,func()接收到的参数为int&  
	std::cout << std::endl;
	
	mytest2(10);//此时T&& t接收到的实参为右值,T被推导为int(并非int&&),t的类型为int&&,func()接收到的参数为int&&
	std::cout << std::endl;
	
	mytest2<int&&>(static_cast<int&&>(30));//此时T&& t接收到的实参为右值,T被显示指定为int&&,t的类型为int&& &&,引用折叠为int&&,func()接收到的参数为int&&   
	std::cout << std::endl;
	
	return 0;
}

运行结果如图:

这里面用到了std::forward<T>()函数,如果void mytest2(T&& t)中不使用func(std::forward<T>(t)),而是直接用func(t)就达不到完美转发的要求;因为形参t必定为左值,func(t)只能接收到左值,所以只会调用void func(int& a)

代码语言:javascript
复制
#include <string>
#include <iostream>
#include <type_traits>

void func(int& a)
{
	std::cout << "lvalue = " << a << std::endl;
}

void func(int&& a)
{
	std::cout << "rvalue = " << a << std::endl;
}

template <typename T>
void mytest2(T&& t)
{
	std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
	std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
	std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;

	//func(std::forward<T>(t));
	func(t);
}

int main()
{	
	int a = 11;
	mytest2(a);//此时t的类型为int&,t为左值,func()接收到的参数为int&  
	std::cout << std::endl;
	
	mytest2(10);//此时t的类型为int&&,t为左值,func()接收到的参数为int&
	std::cout << std::endl;
	
	mytest2<int&&>(static_cast<int&&>(30));//此时t的类型为int&&,t为左值,func()接收到的参数为int&  
	std::cout << std::endl;
	
	return 0;
}

运行结果如图:

std::forward的代码分析:

remove_reference

代码语言:javascript
复制
template<class _Ty>
	struct remove_reference
	{	// remove reference
	typedef _Ty type;
	};

template<class _Ty>
	struct remove_reference<_Ty&>
	{	// remove reference
	typedef _Ty type;
	};

template<class _Ty>
	struct remove_reference<_Ty&&>
	{	// remove rvalue reference
	typedef _Ty type;
	};

std::forward()中用到了remove_referenceremove_reference的作用是去除T中的引用部分,只获取其中的类型部分。无论T是左值还是右值,最后只获取它的类型部分。

std::forward

代码语言:javascript
复制
//匹配左值
template<class _Ty> inline
	constexpr _Ty&& forward(
		typename remove_reference<_Ty>::type& _Arg) _NOEXCEPT
	{	// forward an lvalue as either an lvalue or an rvalue
	return (static_cast<_Ty&&>(_Arg));
	}

forward()无法由接收的实参推导出_Ty的左右值属性:例如当实参为int&(或int&&)时,会匹配到_Ty&& forward(int& _Arg)(或_Ty&& forward(int&& _Arg)),而remove_reference<int>::type&remove_reference<int&>::type&remove_reference<int&&>::type&都为int&,故无法推断_Tyintint&还是int&&,所以使用forward()时需要显示指出_Ty的类型,如forward<int>()forward<int&>()等。

代码语言:javascript
复制
template <typename T>
void mytest2(T&& t)
{
	//std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
	//std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
	//std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;

	//1.当T&& t接收的实参为左值时,T被推导为T&,t的类型为T&,t为左值,此时forward实例化为forward<T&>(T&)
	//2.1  
	//mytest2(10);
	//当T&& t接收的实参为右值时,T被推导为T,t的类型为T&&,t为左值,此时forward实例化为forward<T>(T&)
	//2.2  
	//mytest2<int&&>(static_cast<int&&>(30));
	//当T被显示指定为T&&时(此时形参T&& t只能接收右值实参),t的类型为T&&,t为左值,此时forward实例化为forward<T&&>(T&)    
	func(std::forward<T>(t));
}

代码语言:javascript
复制
//匹配右值
template<class _Ty> inline
	constexpr _Ty&& forward(
		typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
	{	// forward an rvalue as an rvalue
	static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
	return (static_cast<_Ty&&>(_Arg));
	}
	

//调用forward
template <typename T>
void mytest2(T&& t)
{
	//std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
	//std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
	//std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;

	//1.当T&& t接收的实参为左值时,T被推导为T&,t的类型为T&,std::move(t)为右值,此时forward实例化为forward<T&>(T&&)
	//此时因为T被推导为T&,为左值,static_assert断言失败,会报错
	//2.1  
	//mytest2(10);
	//当T&& t接收的实参为右值时,T被推导为T,t的类型为T&&,std::move(t)为右值,此时forward实例化为forward<T>(T&&)
	//2.2  
	//mytest2<int&&>(static_cast<int&&>(30));
	//当T被显示指定为T&&且T&& t接收的实参为右值时,t的类型为T&&,std::move(t)为右值,此时forward实例化为forward<T&&>(T&&)
	func(std::forward<T>(std::move(t)));
}

移动语义

移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。

std::move()

代码语言:javascript
复制
template<class _Ty> inline
    constexpr typename remove_reference<_Ty>::type&&
        move(_Ty&& _Arg) _NOEXCEPT
    {    // forward _Arg as movable
    return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
    }
    
//参数处根据模板推导,得出_Ty类型为_Ty、_Ty&或_Ty&&,所以_Arg可能是_Ty&或者_Ty&&。std::move的功能是:
//传递实参的是左值,推导_Arg为左值引用,仍旧static_cast转换为右值引用。
//传递实参的是右值,推导_Arg为右值引用,仍旧static_cast转换为右值引用。
//在返回处,直接返回右值引用类型即可。

//在《Effective Modern C++》中建议:对于右值引用使用std::move,对于万能引用使用std::forward。
//std::move()与std::forward()都仅仅做了类型转换而已。真正的移动操作是在移动构造函数或者移动赋值操作符中发生的。
//std::move()可以应用于左值(普通的变量int这些使用move与不使用move效果一样),但这么做要谨慎。因为一旦“移动”了左值,就表示当前的值不再需要了,如果后续使用了该值,产生的行为是未定义。

从代码中可以发现:std::move函数将接收的实参强转为了右值引用,仅改变了其左右值属性,并不改变被转化对象本身的数据和其生命周期(这点与std::forward()类似)。调用std::move之后,再在移动构造函数和移动赋值运算符重载函数中实现移动语义。

移动构造函数和移动赋值运算符重载函数

一般配合std::move()使用:

A c = std::move(A("555474", 6));

A a;

a = std::move(A("555474", 6));

A类如下:

代码语言:javascript
复制
class A
{
public:
	A()
	{
		num = 0;
		p = new char[1];
		*p = '\0';

		std::cout << "默认构造函数" << std::endl;
	}

	A(const char* str)
	{
		if (!str)
		{
			num = 0;
			p = new char[1];
			*p = '\0';

			return;
		}

		const char* pp = str;
		num = 0;
		while (*pp != '\0')
		{
			pp++;
			num++;
		}

		p = new char[num + 1];

		memcpy(p, str, num);
		*(p + num) = '\0';

		std::cout << "构造函数1" << std::endl;
	}

	A(const char* str, int size)
	{
		if (!str || size < 1)
		{
			num = 0;
			p = new char[1];
			*p = '\0';

			return;
		}

		num = size;

		p = new char[num + 1];
		memcpy(p, str, num);
		*(p + num) = '\0';

		std::cout << "构造函数2" << std::endl;
	}

	~A()
	{
		num = 0;
		if (p)
		{
			delete[] p;
			p = 0;
		}
		std::cout << "析构函数" << std::endl;
	}

	A(const A& a)
	{
		num = a.num;

		p = new char[num + 1];
		memcpy(p, a.p, num);
		*(p + num) = '\0';

		std::cout << "拷贝构造函数" << std::endl;
	}

	A(A&& a) noexcept
	{
		num = a.num;
		p = a.p;

		a.num = 0;
		a.p = 0;

		std::cout << "移动构造函数" << std::endl;
	}

	A& operator=(const A& a)
	{
		if (this == &a) return *this;

		if (p) delete[] p;

		num = a.num;

		p = new char[num + 1];
		memcpy(p, a.p, num);
		*(p + num) = '\0';

		std::cout << "赋值运算符重载函数" << std::endl;

		return *this;
	}

	A& operator=(A&& a) noexcept
	{
		if (this == &a) return *this;

		if(p) delete[] p;

		num = a.num;
		p = a.p;

		a.num = 0;
		a.p = 0;

		std::cout << "移动赋值运算符重载函数" << std::endl;

		return *this;
	}

	int num;
	char* p;
};

总结

  • 左值可以通过std::move()static_cast<int&&>(lvalue)等转为右值,但右值不能由static_cast<int&>(rvalue)转为左值。
  • 左值引用和右值引用的作用都是减少拷贝,右值引用可以认为是弥补了左值引用的不足之处。
  • 目前右值引用主要是用来实现移动语义std::move()和完美转发std::forward()
  • 右值引用做参数和做返回值时可减少拷贝次数,本质上利用了移动构造和移动赋值。
  • 右值引用和const左值引用可以延长其绑定临时对象的生命周期。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 左值和右值
    • 左值引用和右值引用
      • 万能引用
        • 引用折叠
        • 完美转发
          • std::forward的代码分析:
            • remove_reference
            • std::forward
        • 移动语义
          • std::move()
            • 移动构造函数和移动赋值运算符重载函数
            • 总结
            相关产品与服务
            腾讯云代码分析
            腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档